Skip to content

Commit

Permalink
fix(eslint-plugin): [no-unnecessary-condition] fix false positive wit…
Browse files Browse the repository at this point in the history
…h computed member access and branded key type (#7706)

* fix(eslint-plugin): [no-unnecessary-condition] fix false positive with computed member access and branded key type

* fix(eslint-plugin): [no-unnecessary-condition] add additional test cases for branded key type's index access
  • Loading branch information
yf-yang committed Nov 17, 2023
1 parent cfba320 commit f151b26
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 6 deletions.
9 changes: 3 additions & 6 deletions packages/eslint-plugin/src/rules/no-unnecessary-condition.ts
Expand Up @@ -534,12 +534,9 @@ export default createRule<Options, MessageId>({
}
}
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.
Expand Down
143 changes: 143 additions & 0 deletions packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts
Expand Up @@ -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 extends string> = 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 extends string> = 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 extends string> = 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 extends string> = 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[][] = [];
Expand Down

0 comments on commit f151b26

Please sign in to comment.