Skip to content

Commit 313c93c

Browse files
authored
Merge pull request #17521 from Microsoft/deferLookupTypeResolution
Defer indexed access type resolution
2 parents 48d5485 + 98f6761 commit 313c93c

8 files changed

+433
-19
lines changed

src/compiler/checker.ts

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5788,8 +5788,7 @@ namespace ts {
57885788
}
57895789

57905790
function isGenericMappedType(type: Type) {
5791-
return getObjectFlags(type) & ObjectFlags.Mapped &&
5792-
maybeTypeOfKind(getConstraintTypeFromMappedType(<MappedType>type), TypeFlags.TypeVariable | TypeFlags.Index);
5791+
return getObjectFlags(type) & ObjectFlags.Mapped && isGenericIndexType(getConstraintTypeFromMappedType(<MappedType>type));
57935792
}
57945793

57955794
function resolveStructuredTypeMembers(type: StructuredType): ResolvedType {
@@ -5901,6 +5900,10 @@ namespace ts {
59015900
}
59025901

59035902
function getConstraintOfIndexedAccess(type: IndexedAccessType) {
5903+
const transformed = getTransformedIndexedAccessType(type);
5904+
if (transformed) {
5905+
return transformed;
5906+
}
59045907
const baseObjectType = getBaseConstraintOfType(type.objectType);
59055908
const baseIndexType = getBaseConstraintOfType(type.indexType);
59065909
return baseObjectType || baseIndexType ? getIndexedAccessType(baseObjectType || type.objectType, baseIndexType || type.indexType) : undefined;
@@ -5972,11 +5975,18 @@ namespace ts {
59725975
return stringType;
59735976
}
59745977
if (t.flags & TypeFlags.IndexedAccess) {
5978+
const transformed = getTransformedIndexedAccessType(<IndexedAccessType>t);
5979+
if (transformed) {
5980+
return getBaseConstraint(transformed);
5981+
}
59755982
const baseObjectType = getBaseConstraint((<IndexedAccessType>t).objectType);
59765983
const baseIndexType = getBaseConstraint((<IndexedAccessType>t).indexType);
59775984
const baseIndexedAccess = baseObjectType && baseIndexType ? getIndexedAccessType(baseObjectType, baseIndexType) : undefined;
59785985
return baseIndexedAccess && baseIndexedAccess !== unknownType ? getBaseConstraint(baseIndexedAccess) : undefined;
59795986
}
5987+
if (isGenericMappedType(t)) {
5988+
return emptyObjectType;
5989+
}
59805990
return t;
59815991
}
59825992
}
@@ -7604,26 +7614,73 @@ namespace ts {
76047614
return instantiateType(getTemplateTypeFromMappedType(type), templateMapper);
76057615
}
76067616

7607-
function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) {
7608-
// If the index type is generic, if the object type is generic and doesn't originate in an expression,
7609-
// or if the object type is a mapped type with a generic constraint, we are performing a higher-order
7610-
// index access where we cannot meaningfully access the properties of the object type. Note that for a
7611-
// generic T and a non-generic K, we eagerly resolve T[K] if it originates in an expression. This is to
7612-
// preserve backwards compatibility. For example, an element access 'this["foo"]' has always been resolved
7613-
// eagerly using the constraint type of 'this' at the given location.
7614-
if (maybeTypeOfKind(indexType, TypeFlags.TypeVariable | TypeFlags.Index) ||
7615-
maybeTypeOfKind(objectType, TypeFlags.TypeVariable) && !(accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression) ||
7616-
isGenericMappedType(objectType)) {
7617+
function isGenericObjectType(type: Type): boolean {
7618+
return type.flags & TypeFlags.TypeVariable ? true :
7619+
getObjectFlags(type) & ObjectFlags.Mapped ? isGenericIndexType(getConstraintTypeFromMappedType(<MappedType>type)) :
7620+
type.flags & TypeFlags.UnionOrIntersection ? forEach((<UnionOrIntersectionType>type).types, isGenericObjectType) :
7621+
false;
7622+
}
7623+
7624+
function isGenericIndexType(type: Type): boolean {
7625+
return type.flags & (TypeFlags.TypeVariable | TypeFlags.Index) ? true :
7626+
type.flags & TypeFlags.UnionOrIntersection ? forEach((<UnionOrIntersectionType>type).types, isGenericIndexType) :
7627+
false;
7628+
}
7629+
7630+
// Return true if the given type is a non-generic object type with a string index signature and no
7631+
// other members.
7632+
function isStringIndexOnlyType(type: Type) {
7633+
if (type.flags & TypeFlags.Object && !isGenericMappedType(type)) {
7634+
const t = resolveStructuredTypeMembers(<ObjectType>type);
7635+
return t.properties.length === 0 &&
7636+
t.callSignatures.length === 0 && t.constructSignatures.length === 0 &&
7637+
t.stringIndexInfo && !t.numberIndexInfo;
7638+
}
7639+
return false;
7640+
}
7641+
7642+
// Given an indexed access type T[K], if T is an intersection containing one or more generic types and one or
7643+
// more object types with only a string index signature, e.g. '(U & V & { [x: string]: D })[K]', return a
7644+
// transformed type of the form '(U & V)[K] | D'. This allows us to properly reason about higher order indexed
7645+
// access types with default property values as expressed by D.
7646+
function getTransformedIndexedAccessType(type: IndexedAccessType): Type {
7647+
const objectType = type.objectType;
7648+
if (objectType.flags & TypeFlags.Intersection && isGenericObjectType(objectType) && some((<IntersectionType>objectType).types, isStringIndexOnlyType)) {
7649+
const regularTypes: Type[] = [];
7650+
const stringIndexTypes: Type[] = [];
7651+
for (const t of (<IntersectionType>objectType).types) {
7652+
if (isStringIndexOnlyType(t)) {
7653+
stringIndexTypes.push(getIndexTypeOfType(t, IndexKind.String));
7654+
}
7655+
else {
7656+
regularTypes.push(t);
7657+
}
7658+
}
7659+
return getUnionType([
7660+
getIndexedAccessType(getIntersectionType(regularTypes), type.indexType),
7661+
getIntersectionType(stringIndexTypes)
7662+
]);
7663+
}
7664+
return undefined;
7665+
}
7666+
7667+
function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode): Type {
7668+
// If the object type is a mapped type { [P in K]: E }, where K is generic, we instantiate E using a mapper
7669+
// that substitutes the index type for P. For example, for an index access { [P in K]: Box<T[P]> }[X], we
7670+
// construct the type Box<T[X]>.
7671+
if (isGenericMappedType(objectType)) {
7672+
return getIndexedAccessForMappedType(<MappedType>objectType, indexType, accessNode);
7673+
}
7674+
// Otherwise, if the index type is generic, or if the object type is generic and doesn't originate in an
7675+
// expression, we are performing a higher-order index access where we cannot meaningfully access the properties
7676+
// of the object type. Note that for a generic T and a non-generic K, we eagerly resolve T[K] if it originates
7677+
// in an expression. This is to preserve backwards compatibility. For example, an element access 'this["foo"]'
7678+
// has always been resolved eagerly using the constraint type of 'this' at the given location.
7679+
if (isGenericIndexType(indexType) || !(accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression) && isGenericObjectType(objectType)) {
76177680
if (objectType.flags & TypeFlags.Any) {
76187681
return objectType;
76197682
}
7620-
// If the object type is a mapped type { [P in K]: E }, we instantiate E using a mapper that substitutes
7621-
// the index type for P. For example, for an index access { [P in K]: Box<T[P]> }[X], we construct the
7622-
// type Box<T[X]>.
7623-
if (isGenericMappedType(objectType)) {
7624-
return getIndexedAccessForMappedType(<MappedType>objectType, indexType, accessNode);
7625-
}
7626-
// Otherwise we defer the operation by creating an indexed access type.
7683+
// Defer the operation by creating an indexed access type.
76277684
const id = objectType.id + "," + indexType.id;
76287685
let type = indexedAccessTypes.get(id);
76297686
if (!type) {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//// [deferredLookupTypeResolution.ts]
2+
// Repro from #17456
3+
4+
type StringContains<S extends string, L extends string> = (
5+
{ [K in S]: 'true' } &
6+
{ [key: string]: 'false' }
7+
)[L]
8+
9+
type ObjectHasKey<O, L extends string> = StringContains<keyof O, L>
10+
11+
type First<T> = ObjectHasKey<T, '0'>; // Should be deferred
12+
13+
type T1 = ObjectHasKey<{ a: string }, 'a'>; // 'true'
14+
type T2 = ObjectHasKey<{ a: string }, 'b'>; // 'false'
15+
16+
// Verify that mapped type isn't eagerly resolved in type-to-string operation
17+
18+
declare function f1<A extends string, B extends string>(a: A, b: B): { [P in A | B]: any };
19+
20+
function f2<A extends string>(a: A) {
21+
return f1(a, 'x');
22+
}
23+
24+
function f3(x: 'a' | 'b') {
25+
return f2(x);
26+
}
27+
28+
29+
//// [deferredLookupTypeResolution.js]
30+
"use strict";
31+
// Repro from #17456
32+
function f2(a) {
33+
return f1(a, 'x');
34+
}
35+
function f3(x) {
36+
return f2(x);
37+
}
38+
39+
40+
//// [deferredLookupTypeResolution.d.ts]
41+
declare type StringContains<S extends string, L extends string> = ({
42+
[K in S]: 'true';
43+
} & {
44+
[key: string]: 'false';
45+
})[L];
46+
declare type ObjectHasKey<O, L extends string> = StringContains<keyof O, L>;
47+
declare type First<T> = ObjectHasKey<T, '0'>;
48+
declare type T1 = ObjectHasKey<{
49+
a: string;
50+
}, 'a'>;
51+
declare type T2 = ObjectHasKey<{
52+
a: string;
53+
}, 'b'>;
54+
declare function f1<A extends string, B extends string>(a: A, b: B): {
55+
[P in A | B]: any;
56+
};
57+
declare function f2<A extends string>(a: A): {
58+
[P in A | "x"]: any;
59+
};
60+
declare function f3(x: 'a' | 'b'): {
61+
a: any;
62+
b: any;
63+
x: any;
64+
};
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
=== tests/cases/compiler/deferredLookupTypeResolution.ts ===
2+
// Repro from #17456
3+
4+
type StringContains<S extends string, L extends string> = (
5+
>StringContains : Symbol(StringContains, Decl(deferredLookupTypeResolution.ts, 0, 0))
6+
>S : Symbol(S, Decl(deferredLookupTypeResolution.ts, 2, 20))
7+
>L : Symbol(L, Decl(deferredLookupTypeResolution.ts, 2, 37))
8+
9+
{ [K in S]: 'true' } &
10+
>K : Symbol(K, Decl(deferredLookupTypeResolution.ts, 3, 7))
11+
>S : Symbol(S, Decl(deferredLookupTypeResolution.ts, 2, 20))
12+
13+
{ [key: string]: 'false' }
14+
>key : Symbol(key, Decl(deferredLookupTypeResolution.ts, 4, 7))
15+
16+
)[L]
17+
>L : Symbol(L, Decl(deferredLookupTypeResolution.ts, 2, 37))
18+
19+
type ObjectHasKey<O, L extends string> = StringContains<keyof O, L>
20+
>ObjectHasKey : Symbol(ObjectHasKey, Decl(deferredLookupTypeResolution.ts, 5, 6))
21+
>O : Symbol(O, Decl(deferredLookupTypeResolution.ts, 7, 18))
22+
>L : Symbol(L, Decl(deferredLookupTypeResolution.ts, 7, 20))
23+
>StringContains : Symbol(StringContains, Decl(deferredLookupTypeResolution.ts, 0, 0))
24+
>O : Symbol(O, Decl(deferredLookupTypeResolution.ts, 7, 18))
25+
>L : Symbol(L, Decl(deferredLookupTypeResolution.ts, 7, 20))
26+
27+
type First<T> = ObjectHasKey<T, '0'>; // Should be deferred
28+
>First : Symbol(First, Decl(deferredLookupTypeResolution.ts, 7, 67))
29+
>T : Symbol(T, Decl(deferredLookupTypeResolution.ts, 9, 11))
30+
>ObjectHasKey : Symbol(ObjectHasKey, Decl(deferredLookupTypeResolution.ts, 5, 6))
31+
>T : Symbol(T, Decl(deferredLookupTypeResolution.ts, 9, 11))
32+
33+
type T1 = ObjectHasKey<{ a: string }, 'a'>; // 'true'
34+
>T1 : Symbol(T1, Decl(deferredLookupTypeResolution.ts, 9, 37))
35+
>ObjectHasKey : Symbol(ObjectHasKey, Decl(deferredLookupTypeResolution.ts, 5, 6))
36+
>a : Symbol(a, Decl(deferredLookupTypeResolution.ts, 11, 24))
37+
38+
type T2 = ObjectHasKey<{ a: string }, 'b'>; // 'false'
39+
>T2 : Symbol(T2, Decl(deferredLookupTypeResolution.ts, 11, 43))
40+
>ObjectHasKey : Symbol(ObjectHasKey, Decl(deferredLookupTypeResolution.ts, 5, 6))
41+
>a : Symbol(a, Decl(deferredLookupTypeResolution.ts, 12, 24))
42+
43+
// Verify that mapped type isn't eagerly resolved in type-to-string operation
44+
45+
declare function f1<A extends string, B extends string>(a: A, b: B): { [P in A | B]: any };
46+
>f1 : Symbol(f1, Decl(deferredLookupTypeResolution.ts, 12, 43))
47+
>A : Symbol(A, Decl(deferredLookupTypeResolution.ts, 16, 20))
48+
>B : Symbol(B, Decl(deferredLookupTypeResolution.ts, 16, 37))
49+
>a : Symbol(a, Decl(deferredLookupTypeResolution.ts, 16, 56))
50+
>A : Symbol(A, Decl(deferredLookupTypeResolution.ts, 16, 20))
51+
>b : Symbol(b, Decl(deferredLookupTypeResolution.ts, 16, 61))
52+
>B : Symbol(B, Decl(deferredLookupTypeResolution.ts, 16, 37))
53+
>P : Symbol(P, Decl(deferredLookupTypeResolution.ts, 16, 72))
54+
>A : Symbol(A, Decl(deferredLookupTypeResolution.ts, 16, 20))
55+
>B : Symbol(B, Decl(deferredLookupTypeResolution.ts, 16, 37))
56+
57+
function f2<A extends string>(a: A) {
58+
>f2 : Symbol(f2, Decl(deferredLookupTypeResolution.ts, 16, 91))
59+
>A : Symbol(A, Decl(deferredLookupTypeResolution.ts, 18, 12))
60+
>a : Symbol(a, Decl(deferredLookupTypeResolution.ts, 18, 30))
61+
>A : Symbol(A, Decl(deferredLookupTypeResolution.ts, 18, 12))
62+
63+
return f1(a, 'x');
64+
>f1 : Symbol(f1, Decl(deferredLookupTypeResolution.ts, 12, 43))
65+
>a : Symbol(a, Decl(deferredLookupTypeResolution.ts, 18, 30))
66+
}
67+
68+
function f3(x: 'a' | 'b') {
69+
>f3 : Symbol(f3, Decl(deferredLookupTypeResolution.ts, 20, 1))
70+
>x : Symbol(x, Decl(deferredLookupTypeResolution.ts, 22, 12))
71+
72+
return f2(x);
73+
>f2 : Symbol(f2, Decl(deferredLookupTypeResolution.ts, 16, 91))
74+
>x : Symbol(x, Decl(deferredLookupTypeResolution.ts, 22, 12))
75+
}
76+
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
=== tests/cases/compiler/deferredLookupTypeResolution.ts ===
2+
// Repro from #17456
3+
4+
type StringContains<S extends string, L extends string> = (
5+
>StringContains : ({ [K in S]: "true"; } & { [key: string]: "false"; })[L]
6+
>S : S
7+
>L : L
8+
9+
{ [K in S]: 'true' } &
10+
>K : K
11+
>S : S
12+
13+
{ [key: string]: 'false' }
14+
>key : string
15+
16+
)[L]
17+
>L : L
18+
19+
type ObjectHasKey<O, L extends string> = StringContains<keyof O, L>
20+
>ObjectHasKey : ({ [K in S]: "true"; } & { [key: string]: "false"; })[L]
21+
>O : O
22+
>L : L
23+
>StringContains : ({ [K in S]: "true"; } & { [key: string]: "false"; })[L]
24+
>O : O
25+
>L : L
26+
27+
type First<T> = ObjectHasKey<T, '0'>; // Should be deferred
28+
>First : ({ [K in S]: "true"; } & { [key: string]: "false"; })["0"]
29+
>T : T
30+
>ObjectHasKey : ({ [K in S]: "true"; } & { [key: string]: "false"; })[L]
31+
>T : T
32+
33+
type T1 = ObjectHasKey<{ a: string }, 'a'>; // 'true'
34+
>T1 : "true"
35+
>ObjectHasKey : ({ [K in S]: "true"; } & { [key: string]: "false"; })[L]
36+
>a : string
37+
38+
type T2 = ObjectHasKey<{ a: string }, 'b'>; // 'false'
39+
>T2 : "false"
40+
>ObjectHasKey : ({ [K in S]: "true"; } & { [key: string]: "false"; })[L]
41+
>a : string
42+
43+
// Verify that mapped type isn't eagerly resolved in type-to-string operation
44+
45+
declare function f1<A extends string, B extends string>(a: A, b: B): { [P in A | B]: any };
46+
>f1 : <A extends string, B extends string>(a: A, b: B) => { [P in A | B]: any; }
47+
>A : A
48+
>B : B
49+
>a : A
50+
>A : A
51+
>b : B
52+
>B : B
53+
>P : P
54+
>A : A
55+
>B : B
56+
57+
function f2<A extends string>(a: A) {
58+
>f2 : <A extends string>(a: A) => { [P in A | B]: any; }
59+
>A : A
60+
>a : A
61+
>A : A
62+
63+
return f1(a, 'x');
64+
>f1(a, 'x') : { [P in A | B]: any; }
65+
>f1 : <A extends string, B extends string>(a: A, b: B) => { [P in A | B]: any; }
66+
>a : A
67+
>'x' : "x"
68+
}
69+
70+
function f3(x: 'a' | 'b') {
71+
>f3 : (x: "a" | "b") => { a: any; b: any; x: any; }
72+
>x : "a" | "b"
73+
74+
return f2(x);
75+
>f2(x) : { a: any; b: any; x: any; }
76+
>f2 : <A extends string>(a: A) => { [P in A | B]: any; }
77+
>x : "a" | "b"
78+
}
79+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
tests/cases/compiler/deferredLookupTypeResolution2.ts(14,13): error TS2536: Type '({ [K in S]: "true"; } & { [key: string]: "false"; })["1"]' cannot be used to index type '{ true: "true"; }'.
2+
tests/cases/compiler/deferredLookupTypeResolution2.ts(19,21): error TS2536: Type '({ true: "otherwise"; } & { [k: string]: "true"; })[({ [K in S]: "true"; } & { [key: string]: "false"; })["1"]]' cannot be used to index type '{ true: "true"; }'.
3+
4+
5+
==== tests/cases/compiler/deferredLookupTypeResolution2.ts (2 errors) ====
6+
// Repro from #17456
7+
8+
type StringContains<S extends string, L extends string> = ({ [K in S]: 'true' } & { [key: string]: 'false'})[L];
9+
10+
type ObjectHasKey<O, L extends string> = StringContains<keyof O, L>;
11+
12+
type A<T> = ObjectHasKey<T, '0'>;
13+
14+
type B = ObjectHasKey<[string, number], '1'>; // "true"
15+
type C = ObjectHasKey<[string, number], '2'>; // "false"
16+
type D = A<[string]>; // "true"
17+
18+
// Error, "false" not handled
19+
type E<T> = { true: 'true' }[ObjectHasKey<T, '1'>];
20+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
21+
!!! error TS2536: Type '({ [K in S]: "true"; } & { [key: string]: "false"; })["1"]' cannot be used to index type '{ true: "true"; }'.
22+
23+
type Juxtapose<T> = ({ true: 'otherwise' } & { [k: string]: 'true' })[ObjectHasKey<T, '1'>];
24+
25+
// Error, "otherwise" is missing
26+
type DeepError<T> = { true: 'true' }[Juxtapose<T>];
27+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
28+
!!! error TS2536: Type '({ true: "otherwise"; } & { [k: string]: "true"; })[({ [K in S]: "true"; } & { [key: string]: "false"; })["1"]]' cannot be used to index type '{ true: "true"; }'.
29+
30+
type DeepOK<T> = { true: 'true', otherwise: 'false' }[Juxtapose<T>];
31+

0 commit comments

Comments
 (0)