Skip to content

Commit c289718

Browse files
authored
Defer types like keyof (T & {}) (#49696)
* 'keyof undefined' and 'keyof null same as 'keyof never' * Update tests * Defer types like `keyof (T & {})` * Restore test * Update test * Accept new baselines * Add tests
1 parent 2eaf49f commit c289718

File tree

10 files changed

+325
-16
lines changed

10 files changed

+325
-16
lines changed

src/compiler/checker.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15312,20 +15312,24 @@ m2: ${(this.mapper2 as unknown as DebugTypeMapper).__debugToString().split("\n")
1531215312
* type parameters in the union with a special never type that is treated as a literal in `getReducedType`, we can cause the `getReducedType` logic
1531315313
* to reduce the resulting type if possible (since only intersections with conflicting literal-typed properties are reducible).
1531415314
*/
15315-
function isPossiblyReducibleByInstantiation(type: UnionType): boolean {
15316-
return some(type.types, t => {
15317-
const uniqueFilled = getUniqueLiteralFilledInstantiation(t);
15318-
return getReducedType(uniqueFilled) !== uniqueFilled;
15319-
});
15315+
function isPossiblyReducibleByInstantiation(type: Type): boolean {
15316+
const uniqueFilled = getUniqueLiteralFilledInstantiation(type);
15317+
return getReducedType(uniqueFilled) !== uniqueFilled;
15318+
}
15319+
15320+
function shouldDeferIndexType(type: Type) {
15321+
return !!(type.flags & TypeFlags.InstantiableNonPrimitive ||
15322+
isGenericTupleType(type) ||
15323+
isGenericMappedType(type) && !hasDistributiveNameType(type) ||
15324+
type.flags & TypeFlags.Union && some((type as UnionType).types, isPossiblyReducibleByInstantiation) ||
15325+
type.flags & TypeFlags.Intersection && maybeTypeOfKind(type, TypeFlags.Instantiable) && some((type as IntersectionType).types, isEmptyAnonymousObjectType));
1532015326
}
1532115327

1532215328
function getIndexType(type: Type, stringsOnly = keyofStringsOnly, noIndexSignatures?: boolean): Type {
1532315329
type = getReducedType(type);
15324-
return type.flags & TypeFlags.Union ? isPossiblyReducibleByInstantiation(type as UnionType)
15325-
? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly)
15326-
: getIntersectionType(map((type as UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
15330+
return shouldDeferIndexType(type) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly) :
15331+
type.flags & TypeFlags.Union ? getIntersectionType(map((type as UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
1532715332
type.flags & TypeFlags.Intersection ? getUnionType(map((type as IntersectionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
15328-
type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && !hasDistributiveNameType(type) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly) :
1532915333
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, stringsOnly, noIndexSignatures) :
1533015334
type === wildcardType ? wildcardType :
1533115335
type.flags & TypeFlags.Unknown ? neverType :

tests/baselines/reference/mappedTypes4.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ var x1: DeepReadonlyFoo;
6363

6464
type Z = { a: number };
6565
type Clone<T> = {
66-
[P in keyof (T & {})]: T[P];
66+
[P in keyof (T & {})]: (T & {})[P];
6767
};
6868
type M = Clone<Z>; // M should be { a: number }
6969

@@ -144,7 +144,7 @@ declare type Z = {
144144
a: number;
145145
};
146146
declare type Clone<T> = {
147-
[P in keyof (T & {})]: T[P];
147+
[P in keyof (T & {})]: (T & {})[P];
148148
};
149149
declare type M = Clone<Z>;
150150
declare var z1: Z;

tests/baselines/reference/mappedTypes4.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ type Clone<T> = {
205205
>Clone : Symbol(Clone, Decl(mappedTypes4.ts, 62, 23))
206206
>T : Symbol(T, Decl(mappedTypes4.ts, 63, 11))
207207

208-
[P in keyof (T & {})]: T[P];
208+
[P in keyof (T & {})]: (T & {})[P];
209209
>P : Symbol(P, Decl(mappedTypes4.ts, 64, 3))
210210
>T : Symbol(T, Decl(mappedTypes4.ts, 63, 11))
211211
>T : Symbol(T, Decl(mappedTypes4.ts, 63, 11))

tests/baselines/reference/mappedTypes4.types

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ type Z = { a: number };
160160
type Clone<T> = {
161161
>Clone : Clone<T>
162162

163-
[P in keyof (T & {})]: T[P];
163+
[P in keyof (T & {})]: (T & {})[P];
164164
};
165165
type M = Clone<Z>; // M should be { a: number }
166-
>M : Clone<Z>
166+
>M : { a: number; }
167167

168168
var z1: Z;
169169
>z1 : Z

tests/baselines/reference/unknownControlFlow.errors.txt

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
tests/cases/conformance/types/unknown/unknownControlFlow.ts(18,9): error TS2322: Type 'unknown' is not assignable to type '{}'.
2+
tests/cases/conformance/types/unknown/unknownControlFlow.ts(283,5): error TS2536: Type 'keyof (T & {})' cannot be used to index type 'T'.
3+
tests/cases/conformance/types/unknown/unknownControlFlow.ts(290,11): error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
4+
tests/cases/conformance/types/unknown/unknownControlFlow.ts(291,5): error TS2345: Argument of type 'null' is not assignable to parameter of type 'never'.
5+
tests/cases/conformance/types/unknown/unknownControlFlow.ts(293,5): error TS2345: Argument of type 'null' is not assignable to parameter of type 'never'.
26

37

4-
==== tests/cases/conformance/types/unknown/unknownControlFlow.ts (1 errors) ====
8+
==== tests/cases/conformance/types/unknown/unknownControlFlow.ts (5 errors) ====
59
type T01 = {} & string; // string
610
type T02 = {} & 'a'; // 'a'
711
type T03 = {} & object; // object
@@ -272,4 +276,44 @@ tests/cases/conformance/types/unknown/unknownControlFlow.ts(18,9): error TS2322:
272276
y;
273277
}
274278
}
279+
280+
// We allow an unconstrained object of a generic type `T` to be indexed by a key of type `keyof T`
281+
// without a check that the object is non-undefined and non-null. This is safe because `keyof T`
282+
// is `never` (meaning no possible keys) for any `T` that includes `undefined` or `null`.
283+
284+
function ff1<T>(t: T, k: keyof T) {
285+
t[k];
286+
}
287+
288+
function ff2<T>(t: T & {}, k: keyof T) {
289+
t[k];
290+
}
291+
292+
function ff3<T>(t: T, k: keyof (T & {})) {
293+
t[k]; // Error
294+
~~~~
295+
!!! error TS2536: Type 'keyof (T & {})' cannot be used to index type 'T'.
296+
}
297+
298+
function ff4<T>(t: T & {}, k: keyof (T & {})) {
299+
t[k];
300+
}
301+
302+
ff1(null, 'foo'); // Error
303+
~~~~~
304+
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
305+
ff2(null, 'foo'); // Error
306+
~~~~
307+
!!! error TS2345: Argument of type 'null' is not assignable to parameter of type 'never'.
308+
ff3(null, 'foo');
309+
ff4(null, 'foo'); // Error
310+
~~~~
311+
!!! error TS2345: Argument of type 'null' is not assignable to parameter of type 'never'.
312+
313+
// Repro from #49681
314+
315+
type Foo = { [key: string]: unknown };
316+
type NullableFoo = Foo | undefined;
317+
318+
type Bar<T extends NullableFoo> = NonNullable<T>[string];
275319

tests/baselines/reference/unknownControlFlow.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,38 @@ function foo<T>(x: T | null) {
267267
y;
268268
}
269269
}
270+
271+
// We allow an unconstrained object of a generic type `T` to be indexed by a key of type `keyof T`
272+
// without a check that the object is non-undefined and non-null. This is safe because `keyof T`
273+
// is `never` (meaning no possible keys) for any `T` that includes `undefined` or `null`.
274+
275+
function ff1<T>(t: T, k: keyof T) {
276+
t[k];
277+
}
278+
279+
function ff2<T>(t: T & {}, k: keyof T) {
280+
t[k];
281+
}
282+
283+
function ff3<T>(t: T, k: keyof (T & {})) {
284+
t[k]; // Error
285+
}
286+
287+
function ff4<T>(t: T & {}, k: keyof (T & {})) {
288+
t[k];
289+
}
290+
291+
ff1(null, 'foo'); // Error
292+
ff2(null, 'foo'); // Error
293+
ff3(null, 'foo');
294+
ff4(null, 'foo'); // Error
295+
296+
// Repro from #49681
297+
298+
type Foo = { [key: string]: unknown };
299+
type NullableFoo = Foo | undefined;
300+
301+
type Bar<T extends NullableFoo> = NonNullable<T>[string];
270302

271303

272304
//// [unknownControlFlow.js]
@@ -501,6 +533,25 @@ function foo(x) {
501533
y;
502534
}
503535
}
536+
// We allow an unconstrained object of a generic type `T` to be indexed by a key of type `keyof T`
537+
// without a check that the object is non-undefined and non-null. This is safe because `keyof T`
538+
// is `never` (meaning no possible keys) for any `T` that includes `undefined` or `null`.
539+
function ff1(t, k) {
540+
t[k];
541+
}
542+
function ff2(t, k) {
543+
t[k];
544+
}
545+
function ff3(t, k) {
546+
t[k]; // Error
547+
}
548+
function ff4(t, k) {
549+
t[k];
550+
}
551+
ff1(null, 'foo'); // Error
552+
ff2(null, 'foo'); // Error
553+
ff3(null, 'foo');
554+
ff4(null, 'foo'); // Error
504555

505556

506557
//// [unknownControlFlow.d.ts]
@@ -541,3 +592,12 @@ declare type QQ<T> = NonNullable<NonNullable<NonNullable<T>>>;
541592
declare function f41<T>(a: T): void;
542593
declare function deepEquals<T>(a: T, b: T): boolean;
543594
declare function foo<T>(x: T | null): void;
595+
declare function ff1<T>(t: T, k: keyof T): void;
596+
declare function ff2<T>(t: T & {}, k: keyof T): void;
597+
declare function ff3<T>(t: T, k: keyof (T & {})): void;
598+
declare function ff4<T>(t: T & {}, k: keyof (T & {})): void;
599+
declare type Foo = {
600+
[key: string]: unknown;
601+
};
602+
declare type NullableFoo = Foo | undefined;
603+
declare type Bar<T extends NullableFoo> = NonNullable<T>[string];

tests/baselines/reference/unknownControlFlow.symbols

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,3 +636,88 @@ function foo<T>(x: T | null) {
636636
}
637637
}
638638

639+
// We allow an unconstrained object of a generic type `T` to be indexed by a key of type `keyof T`
640+
// without a check that the object is non-undefined and non-null. This is safe because `keyof T`
641+
// is `never` (meaning no possible keys) for any `T` that includes `undefined` or `null`.
642+
643+
function ff1<T>(t: T, k: keyof T) {
644+
>ff1 : Symbol(ff1, Decl(unknownControlFlow.ts, 267, 1))
645+
>T : Symbol(T, Decl(unknownControlFlow.ts, 273, 13))
646+
>t : Symbol(t, Decl(unknownControlFlow.ts, 273, 16))
647+
>T : Symbol(T, Decl(unknownControlFlow.ts, 273, 13))
648+
>k : Symbol(k, Decl(unknownControlFlow.ts, 273, 21))
649+
>T : Symbol(T, Decl(unknownControlFlow.ts, 273, 13))
650+
651+
t[k];
652+
>t : Symbol(t, Decl(unknownControlFlow.ts, 273, 16))
653+
>k : Symbol(k, Decl(unknownControlFlow.ts, 273, 21))
654+
}
655+
656+
function ff2<T>(t: T & {}, k: keyof T) {
657+
>ff2 : Symbol(ff2, Decl(unknownControlFlow.ts, 275, 1))
658+
>T : Symbol(T, Decl(unknownControlFlow.ts, 277, 13))
659+
>t : Symbol(t, Decl(unknownControlFlow.ts, 277, 16))
660+
>T : Symbol(T, Decl(unknownControlFlow.ts, 277, 13))
661+
>k : Symbol(k, Decl(unknownControlFlow.ts, 277, 26))
662+
>T : Symbol(T, Decl(unknownControlFlow.ts, 277, 13))
663+
664+
t[k];
665+
>t : Symbol(t, Decl(unknownControlFlow.ts, 277, 16))
666+
>k : Symbol(k, Decl(unknownControlFlow.ts, 277, 26))
667+
}
668+
669+
function ff3<T>(t: T, k: keyof (T & {})) {
670+
>ff3 : Symbol(ff3, Decl(unknownControlFlow.ts, 279, 1))
671+
>T : Symbol(T, Decl(unknownControlFlow.ts, 281, 13))
672+
>t : Symbol(t, Decl(unknownControlFlow.ts, 281, 16))
673+
>T : Symbol(T, Decl(unknownControlFlow.ts, 281, 13))
674+
>k : Symbol(k, Decl(unknownControlFlow.ts, 281, 21))
675+
>T : Symbol(T, Decl(unknownControlFlow.ts, 281, 13))
676+
677+
t[k]; // Error
678+
>t : Symbol(t, Decl(unknownControlFlow.ts, 281, 16))
679+
>k : Symbol(k, Decl(unknownControlFlow.ts, 281, 21))
680+
}
681+
682+
function ff4<T>(t: T & {}, k: keyof (T & {})) {
683+
>ff4 : Symbol(ff4, Decl(unknownControlFlow.ts, 283, 1))
684+
>T : Symbol(T, Decl(unknownControlFlow.ts, 285, 13))
685+
>t : Symbol(t, Decl(unknownControlFlow.ts, 285, 16))
686+
>T : Symbol(T, Decl(unknownControlFlow.ts, 285, 13))
687+
>k : Symbol(k, Decl(unknownControlFlow.ts, 285, 26))
688+
>T : Symbol(T, Decl(unknownControlFlow.ts, 285, 13))
689+
690+
t[k];
691+
>t : Symbol(t, Decl(unknownControlFlow.ts, 285, 16))
692+
>k : Symbol(k, Decl(unknownControlFlow.ts, 285, 26))
693+
}
694+
695+
ff1(null, 'foo'); // Error
696+
>ff1 : Symbol(ff1, Decl(unknownControlFlow.ts, 267, 1))
697+
698+
ff2(null, 'foo'); // Error
699+
>ff2 : Symbol(ff2, Decl(unknownControlFlow.ts, 275, 1))
700+
701+
ff3(null, 'foo');
702+
>ff3 : Symbol(ff3, Decl(unknownControlFlow.ts, 279, 1))
703+
704+
ff4(null, 'foo'); // Error
705+
>ff4 : Symbol(ff4, Decl(unknownControlFlow.ts, 283, 1))
706+
707+
// Repro from #49681
708+
709+
type Foo = { [key: string]: unknown };
710+
>Foo : Symbol(Foo, Decl(unknownControlFlow.ts, 292, 17))
711+
>key : Symbol(key, Decl(unknownControlFlow.ts, 296, 14))
712+
713+
type NullableFoo = Foo | undefined;
714+
>NullableFoo : Symbol(NullableFoo, Decl(unknownControlFlow.ts, 296, 38))
715+
>Foo : Symbol(Foo, Decl(unknownControlFlow.ts, 292, 17))
716+
717+
type Bar<T extends NullableFoo> = NonNullable<T>[string];
718+
>Bar : Symbol(Bar, Decl(unknownControlFlow.ts, 297, 35))
719+
>T : Symbol(T, Decl(unknownControlFlow.ts, 299, 9))
720+
>NullableFoo : Symbol(NullableFoo, Decl(unknownControlFlow.ts, 296, 38))
721+
>NonNullable : Symbol(NonNullable, Decl(lib.es5.d.ts, --, --))
722+
>T : Symbol(T, Decl(unknownControlFlow.ts, 299, 9))
723+

tests/baselines/reference/unknownControlFlow.types

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,3 +718,87 @@ function foo<T>(x: T | null) {
718718
}
719719
}
720720

721+
// We allow an unconstrained object of a generic type `T` to be indexed by a key of type `keyof T`
722+
// without a check that the object is non-undefined and non-null. This is safe because `keyof T`
723+
// is `never` (meaning no possible keys) for any `T` that includes `undefined` or `null`.
724+
725+
function ff1<T>(t: T, k: keyof T) {
726+
>ff1 : <T>(t: T, k: keyof T) => void
727+
>t : T
728+
>k : keyof T
729+
730+
t[k];
731+
>t[k] : T[keyof T]
732+
>t : T
733+
>k : keyof T
734+
}
735+
736+
function ff2<T>(t: T & {}, k: keyof T) {
737+
>ff2 : <T>(t: T & {}, k: keyof T) => void
738+
>t : T & {}
739+
>k : keyof T
740+
741+
t[k];
742+
>t[k] : (T & {})[keyof T]
743+
>t : T & {}
744+
>k : keyof T
745+
}
746+
747+
function ff3<T>(t: T, k: keyof (T & {})) {
748+
>ff3 : <T>(t: T, k: keyof (T & {})) => void
749+
>t : T
750+
>k : keyof (T & {})
751+
752+
t[k]; // Error
753+
>t[k] : any
754+
>t : T
755+
>k : keyof (T & {})
756+
}
757+
758+
function ff4<T>(t: T & {}, k: keyof (T & {})) {
759+
>ff4 : <T>(t: T & {}, k: keyof (T & {})) => void
760+
>t : T & {}
761+
>k : keyof (T & {})
762+
763+
t[k];
764+
>t[k] : (T & {})[keyof (T & {})]
765+
>t : T & {}
766+
>k : keyof (T & {})
767+
}
768+
769+
ff1(null, 'foo'); // Error
770+
>ff1(null, 'foo') : void
771+
>ff1 : <T>(t: T, k: keyof T) => void
772+
>null : null
773+
>'foo' : "foo"
774+
775+
ff2(null, 'foo'); // Error
776+
>ff2(null, 'foo') : void
777+
>ff2 : <T>(t: T & {}, k: keyof T) => void
778+
>null : null
779+
>'foo' : "foo"
780+
781+
ff3(null, 'foo');
782+
>ff3(null, 'foo') : void
783+
>ff3 : <T>(t: T, k: keyof (T & {})) => void
784+
>null : null
785+
>'foo' : "foo"
786+
787+
ff4(null, 'foo'); // Error
788+
>ff4(null, 'foo') : void
789+
>ff4 : <T>(t: T & {}, k: keyof (T & {})) => void
790+
>null : null
791+
>'foo' : "foo"
792+
793+
// Repro from #49681
794+
795+
type Foo = { [key: string]: unknown };
796+
>Foo : { [key: string]: unknown; }
797+
>key : string
798+
799+
type NullableFoo = Foo | undefined;
800+
>NullableFoo : Foo | undefined
801+
802+
type Bar<T extends NullableFoo> = NonNullable<T>[string];
803+
>Bar : Bar<T>
804+

tests/cases/conformance/types/mapped/mappedTypes4.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ var x1: DeepReadonlyFoo;
6565

6666
type Z = { a: number };
6767
type Clone<T> = {
68-
[P in keyof (T & {})]: T[P];
68+
[P in keyof (T & {})]: (T & {})[P];
6969
};
7070
type M = Clone<Z>; // M should be { a: number }
7171

0 commit comments

Comments
 (0)