From 728a4fbe42671af534183be292faac32c515f7d0 Mon Sep 17 00:00:00 2001 From: Yifei Date: Sun, 1 Oct 2023 00:27:08 +0800 Subject: [PATCH 1/2] fix(eslint-plugin): [no-unnecessary-condition] fix false positive with computed member access and branded key type --- .../src/rules/no-unnecessary-condition.ts | 9 ++-- .../rules/no-unnecessary-condition.test.ts | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index bc48bfd3dc71..a55fd0d3956a 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 b72eb4f13c19..7071f7fb9dcb 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -507,6 +507,53 @@ type Key = 'bar' | 'foo' | 'baz'; declare const foo: Foo; 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(); `, ` From 3b6693d16d264e836ce7e721f76f096bea595fd0 Mon Sep 17 00:00:00 2001 From: Yifei Date: Mon, 23 Oct 2023 18:20:39 +0800 Subject: [PATCH 2/2] fix(eslint-plugin): [no-unnecessary-condition] add additional test cases for branded key type's index access --- .../rules/no-unnecessary-condition.test.ts | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) 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 7071f7fb9dcb..8c891aea6248 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -556,6 +556,102 @@ 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[][] = [];