Skip to content

Commit 5fbe980

Browse files
authored
Fix noUncheckedIndexedAccess with tuple rest types and generic index types (microsoft#40681)
* Fix noUncheckedIndexedAccess for tuple rest elements * Defer inclusion of undefined for generic indexed access types * Create separate IndexedAccessTypes depending on whether --noUncheckedIndexedAccess applies * Undo accidental export * Parenthesize for clearer precedence
1 parent 950dad9 commit 5fbe980

File tree

7 files changed

+243
-31
lines changed

7 files changed

+243
-31
lines changed

src/compiler/checker.ts

Lines changed: 32 additions & 30 deletions
Large diffs are not rendered by default.

src/compiler/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5323,6 +5323,12 @@ namespace ts {
53235323
export interface IndexedAccessType extends InstantiableType {
53245324
objectType: Type;
53255325
indexType: Type;
5326+
/**
5327+
* @internal
5328+
* Indicates that --noUncheckedIndexedAccess may introduce 'undefined' into
5329+
* the resulting type, depending on how type variable constraints are resolved.
5330+
*/
5331+
noUncheckedIndexedAccessCandidate: boolean;
53265332
constraint?: Type;
53275333
simplifiedForReading?: Type;
53285334
simplifiedForWriting?: Type;

tests/baselines/reference/noUncheckedIndexedAccess.errors.txt

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,16 @@ tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(63,5): error TS2322
4949
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(79,7): error TS2322: Type 'number | boolean | undefined' is not assignable to type 'number | boolean'.
5050
Type 'undefined' is not assignable to type 'number | boolean'.
5151
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(85,1): error TS2322: Type 'undefined' is not assignable to type 'string'.
52+
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(90,7): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
53+
Type 'undefined' is not assignable to type 'string'.
54+
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(98,5): error TS2322: Type 'undefined' is not assignable to type '{ [key: string]: string; a: string; b: string; }[Key]'.
55+
Type 'undefined' is not assignable to type 'string'.
56+
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(99,11): error TS2322: Type '{ [key: string]: string; a: string; b: string; }[Key]' is not assignable to type 'string'.
57+
Type 'string | undefined' is not assignable to type 'string'.
58+
Type 'undefined' is not assignable to type 'string'.
5259

5360

54-
==== tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts (28 errors) ====
61+
==== tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts (31 errors) ====
5562
type CheckBooleanOnly<T extends boolean> = any;
5663
// Validate CheckBooleanOnly works - should error
5764
type T_ERR1 = CheckBooleanOnly<boolean | undefined>;
@@ -216,4 +223,30 @@ tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(85,1): error TS2322
216223
symbolMap[s] = undefined; // Should error
217224
~~~~~~~~~~~~
218225
!!! error TS2322: Type 'undefined' is not assignable to type 'string'.
226+
227+
// Variadic tuples
228+
declare const nonEmptyStringArray: [string, ...string[]];
229+
const variadicOk1: string = nonEmptyStringArray[0]; // Should OK
230+
const variadicError1: string = nonEmptyStringArray[1]; // Should error
231+
~~~~~~~~~~~~~~
232+
!!! error TS2322: Type 'string | undefined' is not assignable to type 'string'.
233+
!!! error TS2322: Type 'undefined' is not assignable to type 'string'.
234+
235+
// Generic index type
236+
declare const myRecord1: { a: string; b: string };
237+
declare const myRecord2: { a: string; b: string, [key: string]: string };
238+
const fn1 = <Key extends keyof typeof myRecord1>(key: Key): string => myRecord1[key]; // Should OK
239+
const fn2 = <Key extends keyof typeof myRecord1>(key: Key): string => myRecord2[key]; // Should OK
240+
const fn3 = <Key extends keyof typeof myRecord2>(key: Key) => {
241+
myRecord2[key] = undefined; // Should error
242+
~~~~~~~~~~~~~~
243+
!!! error TS2322: Type 'undefined' is not assignable to type '{ [key: string]: string; a: string; b: string; }[Key]'.
244+
!!! error TS2322: Type 'undefined' is not assignable to type 'string'.
245+
const v: string = myRecord2[key]; // Should error
246+
~
247+
!!! error TS2322: Type '{ [key: string]: string; a: string; b: string; }[Key]' is not assignable to type 'string'.
248+
!!! error TS2322: Type 'string | undefined' is not assignable to type 'string'.
249+
!!! error TS2322: Type 'undefined' is not assignable to type 'string'.
250+
};
251+
219252

tests/baselines/reference/noUncheckedIndexedAccess.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,22 @@ declare const s: unique symbol;
8484
declare const symbolMap: { [s]: string };
8585
const e15: string = symbolMap[s]; // Should OK
8686
symbolMap[s] = undefined; // Should error
87+
88+
// Variadic tuples
89+
declare const nonEmptyStringArray: [string, ...string[]];
90+
const variadicOk1: string = nonEmptyStringArray[0]; // Should OK
91+
const variadicError1: string = nonEmptyStringArray[1]; // Should error
92+
93+
// Generic index type
94+
declare const myRecord1: { a: string; b: string };
95+
declare const myRecord2: { a: string; b: string, [key: string]: string };
96+
const fn1 = <Key extends keyof typeof myRecord1>(key: Key): string => myRecord1[key]; // Should OK
97+
const fn2 = <Key extends keyof typeof myRecord1>(key: Key): string => myRecord2[key]; // Should OK
98+
const fn3 = <Key extends keyof typeof myRecord2>(key: Key) => {
99+
myRecord2[key] = undefined; // Should error
100+
const v: string = myRecord2[key]; // Should error
101+
};
102+
87103

88104

89105
//// [noUncheckedIndexedAccess.js]
@@ -158,3 +174,11 @@ obj1[z];
158174
var f1 = strMapUnion["foo"];
159175
var e15 = symbolMap[s]; // Should OK
160176
symbolMap[s] = undefined; // Should error
177+
var variadicOk1 = nonEmptyStringArray[0]; // Should OK
178+
var variadicError1 = nonEmptyStringArray[1]; // Should error
179+
var fn1 = function (key) { return myRecord1[key]; }; // Should OK
180+
var fn2 = function (key) { return myRecord2[key]; }; // Should OK
181+
var fn3 = function (key) {
182+
myRecord2[key] = undefined; // Should error
183+
var v = myRecord2[key]; // Should error
184+
};

tests/baselines/reference/noUncheckedIndexedAccess.symbols

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,66 @@ symbolMap[s] = undefined; // Should error
287287
>s : Symbol(s, Decl(noUncheckedIndexedAccess.ts, 81, 13))
288288
>undefined : Symbol(undefined)
289289

290+
// Variadic tuples
291+
declare const nonEmptyStringArray: [string, ...string[]];
292+
>nonEmptyStringArray : Symbol(nonEmptyStringArray, Decl(noUncheckedIndexedAccess.ts, 87, 13))
293+
294+
const variadicOk1: string = nonEmptyStringArray[0]; // Should OK
295+
>variadicOk1 : Symbol(variadicOk1, Decl(noUncheckedIndexedAccess.ts, 88, 5))
296+
>nonEmptyStringArray : Symbol(nonEmptyStringArray, Decl(noUncheckedIndexedAccess.ts, 87, 13))
297+
>0 : Symbol(0)
298+
299+
const variadicError1: string = nonEmptyStringArray[1]; // Should error
300+
>variadicError1 : Symbol(variadicError1, Decl(noUncheckedIndexedAccess.ts, 89, 5))
301+
>nonEmptyStringArray : Symbol(nonEmptyStringArray, Decl(noUncheckedIndexedAccess.ts, 87, 13))
302+
303+
// Generic index type
304+
declare const myRecord1: { a: string; b: string };
305+
>myRecord1 : Symbol(myRecord1, Decl(noUncheckedIndexedAccess.ts, 92, 13))
306+
>a : Symbol(a, Decl(noUncheckedIndexedAccess.ts, 92, 26))
307+
>b : Symbol(b, Decl(noUncheckedIndexedAccess.ts, 92, 37))
308+
309+
declare const myRecord2: { a: string; b: string, [key: string]: string };
310+
>myRecord2 : Symbol(myRecord2, Decl(noUncheckedIndexedAccess.ts, 93, 13))
311+
>a : Symbol(a, Decl(noUncheckedIndexedAccess.ts, 93, 26))
312+
>b : Symbol(b, Decl(noUncheckedIndexedAccess.ts, 93, 37))
313+
>key : Symbol(key, Decl(noUncheckedIndexedAccess.ts, 93, 50))
314+
315+
const fn1 = <Key extends keyof typeof myRecord1>(key: Key): string => myRecord1[key]; // Should OK
316+
>fn1 : Symbol(fn1, Decl(noUncheckedIndexedAccess.ts, 94, 5))
317+
>Key : Symbol(Key, Decl(noUncheckedIndexedAccess.ts, 94, 13))
318+
>myRecord1 : Symbol(myRecord1, Decl(noUncheckedIndexedAccess.ts, 92, 13))
319+
>key : Symbol(key, Decl(noUncheckedIndexedAccess.ts, 94, 49))
320+
>Key : Symbol(Key, Decl(noUncheckedIndexedAccess.ts, 94, 13))
321+
>myRecord1 : Symbol(myRecord1, Decl(noUncheckedIndexedAccess.ts, 92, 13))
322+
>key : Symbol(key, Decl(noUncheckedIndexedAccess.ts, 94, 49))
323+
324+
const fn2 = <Key extends keyof typeof myRecord1>(key: Key): string => myRecord2[key]; // Should OK
325+
>fn2 : Symbol(fn2, Decl(noUncheckedIndexedAccess.ts, 95, 5))
326+
>Key : Symbol(Key, Decl(noUncheckedIndexedAccess.ts, 95, 13))
327+
>myRecord1 : Symbol(myRecord1, Decl(noUncheckedIndexedAccess.ts, 92, 13))
328+
>key : Symbol(key, Decl(noUncheckedIndexedAccess.ts, 95, 49))
329+
>Key : Symbol(Key, Decl(noUncheckedIndexedAccess.ts, 95, 13))
330+
>myRecord2 : Symbol(myRecord2, Decl(noUncheckedIndexedAccess.ts, 93, 13))
331+
>key : Symbol(key, Decl(noUncheckedIndexedAccess.ts, 95, 49))
332+
333+
const fn3 = <Key extends keyof typeof myRecord2>(key: Key) => {
334+
>fn3 : Symbol(fn3, Decl(noUncheckedIndexedAccess.ts, 96, 5))
335+
>Key : Symbol(Key, Decl(noUncheckedIndexedAccess.ts, 96, 13))
336+
>myRecord2 : Symbol(myRecord2, Decl(noUncheckedIndexedAccess.ts, 93, 13))
337+
>key : Symbol(key, Decl(noUncheckedIndexedAccess.ts, 96, 49))
338+
>Key : Symbol(Key, Decl(noUncheckedIndexedAccess.ts, 96, 13))
339+
340+
myRecord2[key] = undefined; // Should error
341+
>myRecord2 : Symbol(myRecord2, Decl(noUncheckedIndexedAccess.ts, 93, 13))
342+
>key : Symbol(key, Decl(noUncheckedIndexedAccess.ts, 96, 49))
343+
>undefined : Symbol(undefined)
344+
345+
const v: string = myRecord2[key]; // Should error
346+
>v : Symbol(v, Decl(noUncheckedIndexedAccess.ts, 98, 9))
347+
>myRecord2 : Symbol(myRecord2, Decl(noUncheckedIndexedAccess.ts, 93, 13))
348+
>key : Symbol(key, Decl(noUncheckedIndexedAccess.ts, 96, 49))
349+
350+
};
351+
352+

tests/baselines/reference/noUncheckedIndexedAccess.types

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,71 @@ symbolMap[s] = undefined; // Should error
351351
>s : unique symbol
352352
>undefined : undefined
353353

354+
// Variadic tuples
355+
declare const nonEmptyStringArray: [string, ...string[]];
356+
>nonEmptyStringArray : [string, ...string[]]
357+
358+
const variadicOk1: string = nonEmptyStringArray[0]; // Should OK
359+
>variadicOk1 : string
360+
>nonEmptyStringArray[0] : string
361+
>nonEmptyStringArray : [string, ...string[]]
362+
>0 : 0
363+
364+
const variadicError1: string = nonEmptyStringArray[1]; // Should error
365+
>variadicError1 : string
366+
>nonEmptyStringArray[1] : string | undefined
367+
>nonEmptyStringArray : [string, ...string[]]
368+
>1 : 1
369+
370+
// Generic index type
371+
declare const myRecord1: { a: string; b: string };
372+
>myRecord1 : { a: string; b: string; }
373+
>a : string
374+
>b : string
375+
376+
declare const myRecord2: { a: string; b: string, [key: string]: string };
377+
>myRecord2 : { [key: string]: string; a: string; b: string; }
378+
>a : string
379+
>b : string
380+
>key : string
381+
382+
const fn1 = <Key extends keyof typeof myRecord1>(key: Key): string => myRecord1[key]; // Should OK
383+
>fn1 : <Key extends "a" | "b">(key: Key) => string
384+
><Key extends keyof typeof myRecord1>(key: Key): string => myRecord1[key] : <Key extends "a" | "b">(key: Key) => string
385+
>myRecord1 : { a: string; b: string; }
386+
>key : Key
387+
>myRecord1[key] : { a: string; b: string; }[Key]
388+
>myRecord1 : { a: string; b: string; }
389+
>key : Key
390+
391+
const fn2 = <Key extends keyof typeof myRecord1>(key: Key): string => myRecord2[key]; // Should OK
392+
>fn2 : <Key extends "a" | "b">(key: Key) => string
393+
><Key extends keyof typeof myRecord1>(key: Key): string => myRecord2[key] : <Key extends "a" | "b">(key: Key) => string
394+
>myRecord1 : { a: string; b: string; }
395+
>key : Key
396+
>myRecord2[key] : { [key: string]: string; a: string; b: string; }[Key]
397+
>myRecord2 : { [key: string]: string; a: string; b: string; }
398+
>key : Key
399+
400+
const fn3 = <Key extends keyof typeof myRecord2>(key: Key) => {
401+
>fn3 : <Key extends string | number>(key: Key) => void
402+
><Key extends keyof typeof myRecord2>(key: Key) => { myRecord2[key] = undefined; // Should error const v: string = myRecord2[key]; // Should error} : <Key extends string | number>(key: Key) => void
403+
>myRecord2 : { [key: string]: string; a: string; b: string; }
404+
>key : Key
405+
406+
myRecord2[key] = undefined; // Should error
407+
>myRecord2[key] = undefined : undefined
408+
>myRecord2[key] : { [key: string]: string; a: string; b: string; }[Key]
409+
>myRecord2 : { [key: string]: string; a: string; b: string; }
410+
>key : Key
411+
>undefined : undefined
412+
413+
const v: string = myRecord2[key]; // Should error
414+
>v : string
415+
>myRecord2[key] : { [key: string]: string; a: string; b: string; }[Key]
416+
>myRecord2 : { [key: string]: string; a: string; b: string; }
417+
>key : Key
418+
419+
};
420+
421+

tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,19 @@ declare const s: unique symbol;
8686
declare const symbolMap: { [s]: string };
8787
const e15: string = symbolMap[s]; // Should OK
8888
symbolMap[s] = undefined; // Should error
89+
90+
// Variadic tuples
91+
declare const nonEmptyStringArray: [string, ...string[]];
92+
const variadicOk1: string = nonEmptyStringArray[0]; // Should OK
93+
const variadicError1: string = nonEmptyStringArray[1]; // Should error
94+
95+
// Generic index type
96+
declare const myRecord1: { a: string; b: string };
97+
declare const myRecord2: { a: string; b: string, [key: string]: string };
98+
const fn1 = <Key extends keyof typeof myRecord1>(key: Key): string => myRecord1[key]; // Should OK
99+
const fn2 = <Key extends keyof typeof myRecord1>(key: Key): string => myRecord2[key]; // Should OK
100+
const fn3 = <Key extends keyof typeof myRecord2>(key: Key) => {
101+
myRecord2[key] = undefined; // Should error
102+
const v: string = myRecord2[key]; // Should error
103+
};
104+

0 commit comments

Comments
 (0)