Skip to content

Commit f151b26

Browse files
authored
fix(eslint-plugin): [no-unnecessary-condition] fix false positive with 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
1 parent cfba320 commit f151b26

File tree

2 files changed

+146
-6
lines changed

2 files changed

+146
-6
lines changed

Diff for: packages/eslint-plugin/src/rules/no-unnecessary-condition.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -534,12 +534,9 @@ export default createRule<Options, MessageId>({
534534
}
535535
}
536536
const typeName = getTypeName(checker, propertyType);
537-
return !!(
538-
(typeName === 'string' &&
539-
checker.getIndexInfoOfType(objType, ts.IndexKind.String)) ||
540-
(typeName === 'number' &&
541-
checker.getIndexInfoOfType(objType, ts.IndexKind.Number))
542-
);
537+
return !!checker
538+
.getIndexInfosOfType(objType)
539+
.find(info => getTypeName(checker, info.keyType) === typeName);
543540
}
544541

545542
// Checks whether a member expression is nullable or not regardless of it's previous node.

Diff for: packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts

+143
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,149 @@ declare const key: Key;
509509
510510
foo?.[key]?.trim();
511511
`,
512+
// https://github.com/typescript-eslint/typescript-eslint/issues/7700
513+
`
514+
type BrandedKey = string & { __brand: string };
515+
type Foo = { [key: BrandedKey]: string } | null;
516+
declare const foo: Foo;
517+
const key = '1' as BrandedKey;
518+
foo?.[key]?.trim();
519+
`,
520+
`
521+
type BrandedKey<S extends string> = S & { __brand: string };
522+
type Foo = { [key: string]: string; foo: 'foo'; bar: 'bar' } | null;
523+
type Key = BrandedKey<'bar'> | BrandedKey<'foo'>;
524+
declare const foo: Foo;
525+
declare const key: Key;
526+
foo?.[key].trim();
527+
`,
528+
`
529+
type BrandedKey = string & { __brand: string };
530+
interface Outer {
531+
inner?: {
532+
[key: BrandedKey]: string | undefined;
533+
};
534+
}
535+
function Foo(outer: Outer, key: BrandedKey): number | undefined {
536+
return outer.inner?.[key]?.charCodeAt(0);
537+
}
538+
`,
539+
`
540+
interface Outer {
541+
inner?: {
542+
[key: string & { __brand: string }]: string | undefined;
543+
bar: 'bar';
544+
};
545+
}
546+
type Foo = 'foo' & { __brand: string };
547+
function Foo(outer: Outer, key: Foo): number | undefined {
548+
return outer.inner?.[key]?.charCodeAt(0);
549+
}
550+
`,
551+
`
552+
type BrandedKey<S extends string> = S & { __brand: string };
553+
type Foo = { [key: string]: string; foo: 'foo'; bar: 'bar' } | null;
554+
type Key = BrandedKey<'bar'> | BrandedKey<'foo'> | BrandedKey<'baz'>;
555+
declare const foo: Foo;
556+
declare const key: Key;
557+
foo?.[key]?.trim();
558+
`,
559+
{
560+
code: `
561+
type BrandedKey = string & { __brand: string };
562+
type Foo = { [key: BrandedKey]: string } | null;
563+
declare const foo: Foo;
564+
const key = '1' as BrandedKey;
565+
foo?.[key]?.trim();
566+
`,
567+
parserOptions: {
568+
EXPERIMENTAL_useProjectService: false,
569+
tsconfigRootDir: getFixturesRootDir(),
570+
project: './tsconfig.noUncheckedIndexedAccess.json',
571+
},
572+
dependencyConstraints: {
573+
typescript: '4.1',
574+
},
575+
},
576+
{
577+
code: `
578+
type BrandedKey<S extends string> = S & { __brand: string };
579+
type Foo = { [key: string]: string; foo: 'foo'; bar: 'bar' } | null;
580+
type Key = BrandedKey<'bar'> | BrandedKey<'foo'>;
581+
declare const foo: Foo;
582+
declare const key: Key;
583+
foo?.[key].trim();
584+
`,
585+
parserOptions: {
586+
EXPERIMENTAL_useProjectService: false,
587+
tsconfigRootDir: getFixturesRootDir(),
588+
project: './tsconfig.noUncheckedIndexedAccess.json',
589+
},
590+
dependencyConstraints: {
591+
typescript: '4.1',
592+
},
593+
},
594+
{
595+
code: `
596+
type BrandedKey = string & { __brand: string };
597+
interface Outer {
598+
inner?: {
599+
[key: BrandedKey]: string | undefined;
600+
};
601+
}
602+
function Foo(outer: Outer, key: BrandedKey): number | undefined {
603+
return outer.inner?.[key]?.charCodeAt(0);
604+
}
605+
`,
606+
parserOptions: {
607+
EXPERIMENTAL_useProjectService: false,
608+
tsconfigRootDir: getFixturesRootDir(),
609+
project: './tsconfig.noUncheckedIndexedAccess.json',
610+
},
611+
dependencyConstraints: {
612+
typescript: '4.1',
613+
},
614+
},
615+
{
616+
code: `
617+
interface Outer {
618+
inner?: {
619+
[key: string & { __brand: string }]: string | undefined;
620+
bar: 'bar';
621+
};
622+
}
623+
type Foo = 'foo' & { __brand: string };
624+
function Foo(outer: Outer, key: Foo): number | undefined {
625+
return outer.inner?.[key]?.charCodeAt(0);
626+
}
627+
`,
628+
parserOptions: {
629+
EXPERIMENTAL_useProjectService: false,
630+
tsconfigRootDir: getFixturesRootDir(),
631+
project: './tsconfig.noUncheckedIndexedAccess.json',
632+
},
633+
dependencyConstraints: {
634+
typescript: '4.1',
635+
},
636+
},
637+
{
638+
code: `
639+
type BrandedKey<S extends string> = S & { __brand: string };
640+
type Foo = { [key: string]: string; foo: 'foo'; bar: 'bar' } | null;
641+
type Key = BrandedKey<'bar'> | BrandedKey<'foo'> | BrandedKey<'baz'>;
642+
declare const foo: Foo;
643+
declare const key: Key;
644+
foo?.[key]?.trim();
645+
`,
646+
parserOptions: {
647+
EXPERIMENTAL_useProjectService: false,
648+
tsconfigRootDir: getFixturesRootDir(),
649+
project: './tsconfig.noUncheckedIndexedAccess.json',
650+
},
651+
dependencyConstraints: {
652+
typescript: '4.1',
653+
},
654+
},
512655
`
513656
let latencies: number[][] = [];
514657

0 commit comments

Comments
 (0)