Skip to content

Commit 4209362

Browse files
authored
fix(eslint-plugin): [non-nullable-type-assertion-style] fix false positive when asserting to a generic type that might be nullish (#4509)
1 parent 5ab1d57 commit 4209362

File tree

3 files changed

+81
-5
lines changed

3 files changed

+81
-5
lines changed

Diff for: packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts

+24-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default util.createRule({
1717
fixable: 'code',
1818
messages: {
1919
preferNonNullAssertion:
20-
'Use a ! assertion to more succintly remove null and undefined from the type.',
20+
'Use a ! assertion to more succinctly remove null and undefined from the type.',
2121
},
2222
schema: [],
2323
type: 'suggestion',
@@ -43,22 +43,42 @@ export default util.createRule({
4343
return tsutils.unionTypeParts(type);
4444
};
4545

46+
const couldBeNullish = (type: ts.Type): boolean => {
47+
if (type.flags & ts.TypeFlags.TypeParameter) {
48+
const constraint = type.getConstraint();
49+
return constraint == null || couldBeNullish(constraint);
50+
} else if (tsutils.isUnionType(type)) {
51+
for (const part of type.types) {
52+
if (couldBeNullish(part)) {
53+
return true;
54+
}
55+
}
56+
return false;
57+
} else {
58+
return (
59+
(type.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0
60+
);
61+
}
62+
};
63+
4664
const sameTypeWithoutNullish = (
4765
assertedTypes: ts.Type[],
4866
originalTypes: ts.Type[],
4967
): boolean => {
5068
const nonNullishOriginalTypes = originalTypes.filter(
5169
type =>
52-
type.flags !== ts.TypeFlags.Null &&
53-
type.flags !== ts.TypeFlags.Undefined,
70+
(type.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) === 0,
5471
);
5572

5673
if (nonNullishOriginalTypes.length === originalTypes.length) {
5774
return false;
5875
}
5976

6077
for (const assertedType of assertedTypes) {
61-
if (!nonNullishOriginalTypes.includes(assertedType)) {
78+
if (
79+
couldBeNullish(assertedType) ||
80+
!nonNullishOriginalTypes.includes(assertedType)
81+
) {
6282
return false;
6383
}
6484
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"noUncheckedIndexedAccess": true
5+
}
6+
}

Diff for: packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts

+51-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const ruleTester = new RuleTester({
77
parserOptions: {
88
sourceType: 'module',
99
tsconfigRootDir: rootDir,
10-
project: './tsconfig.json',
10+
project: './tsconfig.noUncheckedIndexedAccess.json',
1111
},
1212
parser: '@typescript-eslint/parser',
1313
});
@@ -61,6 +61,35 @@ const x = 1 as 1;
6161
declare function foo<T = any>(): T;
6262
const bar = foo() as number;
6363
`,
64+
`
65+
function first<T>(array: ArrayLike<T>): T | null {
66+
return array.length > 0 ? (array[0] as T) : null;
67+
}
68+
`,
69+
`
70+
function first<T extends string | null>(array: ArrayLike<T>): T | null {
71+
return array.length > 0 ? (array[0] as T) : null;
72+
}
73+
`,
74+
`
75+
function first<T extends string | undefined>(array: ArrayLike<T>): T | null {
76+
return array.length > 0 ? (array[0] as T) : null;
77+
}
78+
`,
79+
`
80+
function first<T extends string | null | undefined>(
81+
array: ArrayLike<T>,
82+
): T | null {
83+
return array.length > 0 ? (array[0] as T) : null;
84+
}
85+
`,
86+
`
87+
type A = 'a' | 'A';
88+
type B = 'b' | 'B';
89+
function first<T extends A | B | null>(array: ArrayLike<T>): T | null {
90+
return array.length > 0 ? (array[0] as T) : null;
91+
}
92+
`,
6493
],
6594

6695
invalid: [
@@ -199,5 +228,26 @@ declare const x: T;
199228
const y = x!;
200229
`,
201230
},
231+
{
232+
code: `
233+
function first<T extends string | number>(array: ArrayLike<T>): T | null {
234+
return array.length > 0 ? (array[0] as T) : null;
235+
}
236+
`,
237+
errors: [
238+
{
239+
column: 30,
240+
line: 3,
241+
messageId: 'preferNonNullAssertion',
242+
},
243+
],
244+
// Output is not expected to match required formatting due to excess parentheses
245+
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
246+
output: `
247+
function first<T extends string | number>(array: ArrayLike<T>): T | null {
248+
return array.length > 0 ? (array[0]!) : null;
249+
}
250+
`,
251+
},
202252
],
203253
});

0 commit comments

Comments
 (0)