diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index bc48bfd3dc7..a55fd0d3956 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -534,12 +534,9 @@ export default createRule({ } } const typeName = getTypeName(checker, propertyType); - return !!( - (typeName === 'string' && - checker.getIndexInfoOfType(objType, ts.IndexKind.String)) || - (typeName === 'number' && - checker.getIndexInfoOfType(objType, ts.IndexKind.Number)) - ); + return !!checker + .getIndexInfosOfType(objType) + .find(info => getTypeName(checker, info.keyType) === typeName); } // Checks whether a member expression is nullable or not regardless of it's previous node. diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index b72eb4f13c1..8c891aea624 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -509,6 +509,149 @@ declare const key: Key; foo?.[key]?.trim(); `, + // https://github.com/typescript-eslint/typescript-eslint/issues/7700 + ` +type BrandedKey = string & { __brand: string }; +type Foo = { [key: BrandedKey]: string } | null; +declare const foo: Foo; +const key = '1' as BrandedKey; +foo?.[key]?.trim(); + `, + ` +type BrandedKey = S & { __brand: string }; +type Foo = { [key: string]: string; foo: 'foo'; bar: 'bar' } | null; +type Key = BrandedKey<'bar'> | BrandedKey<'foo'>; +declare const foo: Foo; +declare const key: Key; +foo?.[key].trim(); + `, + ` +type BrandedKey = string & { __brand: string }; +interface Outer { + inner?: { + [key: BrandedKey]: string | undefined; + }; +} +function Foo(outer: Outer, key: BrandedKey): number | undefined { + return outer.inner?.[key]?.charCodeAt(0); +} + `, + ` +interface Outer { + inner?: { + [key: string & { __brand: string }]: string | undefined; + bar: 'bar'; + }; +} +type Foo = 'foo' & { __brand: string }; +function Foo(outer: Outer, key: Foo): number | undefined { + return outer.inner?.[key]?.charCodeAt(0); +} + `, + ` +type BrandedKey = S & { __brand: string }; +type Foo = { [key: string]: string; foo: 'foo'; bar: 'bar' } | null; +type Key = BrandedKey<'bar'> | BrandedKey<'foo'> | BrandedKey<'baz'>; +declare const foo: Foo; +declare const key: Key; +foo?.[key]?.trim(); + `, + { + code: ` +type BrandedKey = string & { __brand: string }; +type Foo = { [key: BrandedKey]: string } | null; +declare const foo: Foo; +const key = '1' as BrandedKey; +foo?.[key]?.trim(); + `, + parserOptions: { + EXPERIMENTAL_useProjectService: false, + tsconfigRootDir: getFixturesRootDir(), + project: './tsconfig.noUncheckedIndexedAccess.json', + }, + dependencyConstraints: { + typescript: '4.1', + }, + }, + { + code: ` +type BrandedKey = S & { __brand: string }; +type Foo = { [key: string]: string; foo: 'foo'; bar: 'bar' } | null; +type Key = BrandedKey<'bar'> | BrandedKey<'foo'>; +declare const foo: Foo; +declare const key: Key; +foo?.[key].trim(); + `, + parserOptions: { + EXPERIMENTAL_useProjectService: false, + tsconfigRootDir: getFixturesRootDir(), + project: './tsconfig.noUncheckedIndexedAccess.json', + }, + dependencyConstraints: { + typescript: '4.1', + }, + }, + { + code: ` +type BrandedKey = string & { __brand: string }; +interface Outer { + inner?: { + [key: BrandedKey]: string | undefined; + }; +} +function Foo(outer: Outer, key: BrandedKey): number | undefined { + return outer.inner?.[key]?.charCodeAt(0); +} + `, + parserOptions: { + EXPERIMENTAL_useProjectService: false, + tsconfigRootDir: getFixturesRootDir(), + project: './tsconfig.noUncheckedIndexedAccess.json', + }, + dependencyConstraints: { + typescript: '4.1', + }, + }, + { + code: ` +interface Outer { + inner?: { + [key: string & { __brand: string }]: string | undefined; + bar: 'bar'; + }; +} +type Foo = 'foo' & { __brand: string }; +function Foo(outer: Outer, key: Foo): number | undefined { + return outer.inner?.[key]?.charCodeAt(0); +} + `, + parserOptions: { + EXPERIMENTAL_useProjectService: false, + tsconfigRootDir: getFixturesRootDir(), + project: './tsconfig.noUncheckedIndexedAccess.json', + }, + dependencyConstraints: { + typescript: '4.1', + }, + }, + { + code: ` +type BrandedKey = S & { __brand: string }; +type Foo = { [key: string]: string; foo: 'foo'; bar: 'bar' } | null; +type Key = BrandedKey<'bar'> | BrandedKey<'foo'> | BrandedKey<'baz'>; +declare const foo: Foo; +declare const key: Key; +foo?.[key]?.trim(); + `, + parserOptions: { + EXPERIMENTAL_useProjectService: false, + tsconfigRootDir: getFixturesRootDir(), + project: './tsconfig.noUncheckedIndexedAccess.json', + }, + dependencyConstraints: { + typescript: '4.1', + }, + }, ` let latencies: number[][] = [];