Skip to content

Commit 5dfac15

Browse files
committed
Improve type relation for recursive mapped types
Recursive mapped types usually lead to the error "excess stack depth comparing types" because of a new type relation rule added in #19564 which says that "A source type T is related to a target type { [P in keyof T]: X } if T[P] is related to X". Unfortunately, with self-recursive mapped types like ```ts D<T> = { [P in keyof T]: D<T[P]> } ``` we get infinite recursion when trying to assign a type parameter T to D<T>, as T[P] is compared to D<T[P]>, T[P][P] is compared to D<T[P][P]>, and so on. We can avoid many of these infinite recursions by replacing occurrences of D in the template type with its type argument. This works because mapped types will completely cover the tree, so checking assignability of the top level implies that checking of lower level would succeed, even if there are infinitely many levels. For example: ```ts D<T> = { [P in keyof T]: D<T[P]> | undefined } <T>(t: T, dt: D<T>) => { dt = t } ``` would previously check that `T[P]` is assignable to `D<T[P]> | undefined`. Now, after replacement, it checks that `T[P]` is assignable to `T[P] | undefined`. This implementation suffers from 3 limitations: 1. I use aliasSymbol to detect whether a type reference is a self-recursive one. This only works when the mapped type is at the top level of a type alias. 2. Not all instances of D<T> are replaced with T, just those in intersections and unions. I think this covers almost all uses. 3. This doesn't fix #21048, which tries to assign an "off-by-one" partial-deep type to itself. Mostly fixes #21592. One repro there has a type alias to a union, and a mapped type is a member of the union. But this can be split into two aliases: ```ts type SafeAnyMap<T> = { [K in keyof T]?: SafeAny<T[K] }; type SafeAny<T> = SafeAnyMap<T> | boolean | string | symbol | number | null | undefined; ```
1 parent 162a273 commit 5dfac15

File tree

6 files changed

+358
-12
lines changed

6 files changed

+358
-12
lines changed

src/compiler/checker.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10131,21 +10131,22 @@ namespace ts {
1013110131
const template = getTemplateTypeFromMappedType(target);
1013210132
const modifiers = getMappedTypeModifiers(target);
1013310133
if (!(modifiers & MappedTypeModifiers.ExcludeOptional)) {
10134-
if (template.flags & TypeFlags.IndexedAccess && (<IndexedAccessType>template).objectType === source &&
10135-
(<IndexedAccessType>template).indexType === getTypeParameterFromMappedType(target)) {
10136-
return Ternary.True;
10137-
}
10138-
// A source type T is related to a target type { [P in keyof T]: X } if T[P] is related to X.
10139-
if (!isGenericMappedType(source) && getConstraintTypeFromMappedType(target) === getIndexType(source)) {
10140-
const indexedAccessType = getIndexedAccessType(source, getTypeParameterFromMappedType(target));
10141-
const templateType = getTemplateTypeFromMappedType(target);
10142-
if (result = isRelatedTo(indexedAccessType, templateType, reportErrors)) {
10143-
errorInfo = saveErrorInfo;
10144-
return result;
10134+
if (template.flags & TypeFlags.IndexedAccess && (<IndexedAccessType>template).objectType === source &&
10135+
(<IndexedAccessType>template).indexType === getTypeParameterFromMappedType(target)) {
10136+
return Ternary.True;
10137+
}
10138+
// A source type T is related to a target type { [P in keyof T]: X } if T[P] is related to X
10139+
// *after* occurrences of D<T[P]> in X are replaced with T[P].
10140+
if (!isGenericMappedType(source) && getConstraintTypeFromMappedType(target) === getIndexType(source)) {
10141+
const indexedAccessType = getIndexedAccessType(source, getTypeParameterFromMappedType(target));
10142+
const templateType = replaceRecursiveAliasReference(getTemplateTypeFromMappedType(target), target);
10143+
if (result = isRelatedTo(indexedAccessType, templateType, reportErrors)) {
10144+
errorInfo = saveErrorInfo;
10145+
return result;
10146+
}
1014510147
}
1014610148
}
1014710149
}
10148-
}
1014910150

1015010151
if (source.flags & TypeFlags.TypeParameter) {
1015110152
let constraint = getConstraintForRelation(<TypeParameter>source);
@@ -10292,6 +10293,18 @@ namespace ts {
1029210293
return Ternary.False;
1029310294
}
1029410295

10296+
function replaceRecursiveAliasReference(template: Type, target: Type): Type {
10297+
return mapType(template, t => {
10298+
if (t.flags & TypeFlags.Intersection) {
10299+
return getIntersectionType((t as IntersectionType).types.map(u => replaceRecursiveAliasReference(u, target)))
10300+
}
10301+
const hasSharedAliasSymbol = t.aliasSymbol !== undefined &&
10302+
t.aliasTypeArguments && t.aliasTypeArguments.length === 1 &&
10303+
t.aliasSymbol === target.aliasSymbol;
10304+
return hasSharedAliasSymbol ? t.aliasTypeArguments[0] : t;
10305+
});
10306+
}
10307+
1029510308
// A type [P in S]: X is related to a type [Q in T]: Y if T is related to S and X' is
1029610309
// related to Y, where X' is an instantiation of X in which P is replaced with Q. Notice
1029710310
// that S and T are contra-variant whereas X and Y are co-variant.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
tests/cases/compiler/recursiveMappedTypeAssignability.ts(17,49): error TS2321: Excessive stack depth comparing types 'T[K]' and 'SafeAny<T[K]>'.
2+
3+
4+
==== tests/cases/compiler/recursiveMappedTypeAssignability.ts (1 errors) ====
5+
// type D<U> = { [P in keyof U]: D<U[P]> };
6+
// <T>(t: T, dt: D<T>) => { dt = t };
7+
// type DR<U> = { readonly [P in keyof U]: DR<U[P]> };
8+
// <T>(t: T, dt: DR<T>) => { dt = t };
9+
// type DP<U> = { [P in keyof U]?: DP<U[P]> };
10+
// <T>(t: T, dt: DP<T>) => { dt = t };
11+
// type DAP<U> = { [P in keyof U]?: DAP<U[P]> & U[P] };
12+
// <T>(t: T, dt: DAP<T>) => { dt = t };
13+
14+
// #21592
15+
// doesn't work because aliasSymbol isn't set on the literal type
16+
// since it's not top-level -- the union is.
17+
type SafeAny<T> = {
18+
[K in keyof T]?: SafeAny<T[K]>
19+
} | boolean | number | string | symbol | null | undefined
20+
type DataValidator<T> = {
21+
[K in keyof T]?: (v: SafeAny<T[K]>) => v is T[K]
22+
~~~~
23+
!!! error TS2321: Excessive stack depth comparing types 'T[K]' and 'SafeAny<T[K]>'.
24+
}
25+
26+
// modified repro with top-level mapped type, which works
27+
// because the literal type has aliasSymbol set
28+
type SafeAny2<T> = {
29+
[K in keyof T]?: SafeAny2<T[K]>
30+
}
31+
<T>(t: T, sat: SafeAny2<T>) => { sat = t }
32+
33+
34+
const fn = <T>(arg: T) => {
35+
((arg2: RecursivePartial<T>) => {
36+
// ...
37+
})(arg);
38+
};
39+
40+
type RecursivePartial<T> = {
41+
[P in keyof T]?: RecursivePartial<T[P]>;
42+
};
43+
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//// [recursiveMappedTypeAssignability.ts]
2+
// type D<U> = { [P in keyof U]: D<U[P]> };
3+
// <T>(t: T, dt: D<T>) => { dt = t };
4+
// type DR<U> = { readonly [P in keyof U]: DR<U[P]> };
5+
// <T>(t: T, dt: DR<T>) => { dt = t };
6+
// type DP<U> = { [P in keyof U]?: DP<U[P]> };
7+
// <T>(t: T, dt: DP<T>) => { dt = t };
8+
// type DAP<U> = { [P in keyof U]?: DAP<U[P]> & U[P] };
9+
// <T>(t: T, dt: DAP<T>) => { dt = t };
10+
11+
// #21592
12+
// doesn't work because aliasSymbol isn't set on the literal type
13+
// since it's not top-level -- the union is.
14+
type SafeAny<T> = {
15+
[K in keyof T]?: SafeAny<T[K]>
16+
} | boolean | number | string | symbol | null | undefined
17+
type DataValidator<T> = {
18+
[K in keyof T]?: (v: SafeAny<T[K]>) => v is T[K]
19+
}
20+
21+
// modified repro with top-level mapped type, which works
22+
// because the literal type has aliasSymbol set
23+
type SafeAny2<T> = {
24+
[K in keyof T]?: SafeAny2<T[K]>
25+
}
26+
<T>(t: T, sat: SafeAny2<T>) => { sat = t }
27+
28+
29+
const fn = <T>(arg: T) => {
30+
((arg2: RecursivePartial<T>) => {
31+
// ...
32+
})(arg);
33+
};
34+
35+
type RecursivePartial<T> = {
36+
[P in keyof T]?: RecursivePartial<T[P]>;
37+
};
38+
39+
40+
//// [recursiveMappedTypeAssignability.js]
41+
"use strict";
42+
// type D<U> = { [P in keyof U]: D<U[P]> };
43+
// <T>(t: T, dt: D<T>) => { dt = t };
44+
// type DR<U> = { readonly [P in keyof U]: DR<U[P]> };
45+
// <T>(t: T, dt: DR<T>) => { dt = t };
46+
// type DP<U> = { [P in keyof U]?: DP<U[P]> };
47+
// <T>(t: T, dt: DP<T>) => { dt = t };
48+
// type DAP<U> = { [P in keyof U]?: DAP<U[P]> & U[P] };
49+
// <T>(t: T, dt: DAP<T>) => { dt = t };
50+
(function (t, sat) { sat = t; });
51+
var fn = function (arg) {
52+
(function (arg2) {
53+
// ...
54+
})(arg);
55+
};
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
=== tests/cases/compiler/recursiveMappedTypeAssignability.ts ===
2+
// type D<U> = { [P in keyof U]: D<U[P]> };
3+
// <T>(t: T, dt: D<T>) => { dt = t };
4+
// type DR<U> = { readonly [P in keyof U]: DR<U[P]> };
5+
// <T>(t: T, dt: DR<T>) => { dt = t };
6+
// type DP<U> = { [P in keyof U]?: DP<U[P]> };
7+
// <T>(t: T, dt: DP<T>) => { dt = t };
8+
// type DAP<U> = { [P in keyof U]?: DAP<U[P]> & U[P] };
9+
// <T>(t: T, dt: DAP<T>) => { dt = t };
10+
11+
// #21592
12+
// doesn't work because aliasSymbol isn't set on the literal type
13+
// since it's not top-level -- the union is.
14+
type SafeAny<T> = {
15+
>SafeAny : Symbol(SafeAny, Decl(recursiveMappedTypeAssignability.ts, 0, 0))
16+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 12, 13))
17+
18+
[K in keyof T]?: SafeAny<T[K]>
19+
>K : Symbol(K, Decl(recursiveMappedTypeAssignability.ts, 13, 5))
20+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 12, 13))
21+
>SafeAny : Symbol(SafeAny, Decl(recursiveMappedTypeAssignability.ts, 0, 0))
22+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 12, 13))
23+
>K : Symbol(K, Decl(recursiveMappedTypeAssignability.ts, 13, 5))
24+
25+
} | boolean | number | string | symbol | null | undefined
26+
type DataValidator<T> = {
27+
>DataValidator : Symbol(DataValidator, Decl(recursiveMappedTypeAssignability.ts, 14, 57))
28+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 15, 19))
29+
30+
[K in keyof T]?: (v: SafeAny<T[K]>) => v is T[K]
31+
>K : Symbol(K, Decl(recursiveMappedTypeAssignability.ts, 16, 5))
32+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 15, 19))
33+
>v : Symbol(v, Decl(recursiveMappedTypeAssignability.ts, 16, 22))
34+
>SafeAny : Symbol(SafeAny, Decl(recursiveMappedTypeAssignability.ts, 0, 0))
35+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 15, 19))
36+
>K : Symbol(K, Decl(recursiveMappedTypeAssignability.ts, 16, 5))
37+
>v : Symbol(v, Decl(recursiveMappedTypeAssignability.ts, 16, 22))
38+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 15, 19))
39+
>K : Symbol(K, Decl(recursiveMappedTypeAssignability.ts, 16, 5))
40+
}
41+
42+
// modified repro with top-level mapped type, which works
43+
// because the literal type has aliasSymbol set
44+
type SafeAny2<T> = {
45+
>SafeAny2 : Symbol(SafeAny2, Decl(recursiveMappedTypeAssignability.ts, 17, 1))
46+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 21, 14))
47+
48+
[K in keyof T]?: SafeAny2<T[K]>
49+
>K : Symbol(K, Decl(recursiveMappedTypeAssignability.ts, 22, 5))
50+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 21, 14))
51+
>SafeAny2 : Symbol(SafeAny2, Decl(recursiveMappedTypeAssignability.ts, 17, 1))
52+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 21, 14))
53+
>K : Symbol(K, Decl(recursiveMappedTypeAssignability.ts, 22, 5))
54+
}
55+
<T>(t: T, sat: SafeAny2<T>) => { sat = t }
56+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 24, 1))
57+
>t : Symbol(t, Decl(recursiveMappedTypeAssignability.ts, 24, 4))
58+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 24, 1))
59+
>sat : Symbol(sat, Decl(recursiveMappedTypeAssignability.ts, 24, 9))
60+
>SafeAny2 : Symbol(SafeAny2, Decl(recursiveMappedTypeAssignability.ts, 17, 1))
61+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 24, 1))
62+
>sat : Symbol(sat, Decl(recursiveMappedTypeAssignability.ts, 24, 9))
63+
>t : Symbol(t, Decl(recursiveMappedTypeAssignability.ts, 24, 4))
64+
65+
66+
const fn = <T>(arg: T) => {
67+
>fn : Symbol(fn, Decl(recursiveMappedTypeAssignability.ts, 27, 5))
68+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 27, 12))
69+
>arg : Symbol(arg, Decl(recursiveMappedTypeAssignability.ts, 27, 15))
70+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 27, 12))
71+
72+
((arg2: RecursivePartial<T>) => {
73+
>arg2 : Symbol(arg2, Decl(recursiveMappedTypeAssignability.ts, 28, 6))
74+
>RecursivePartial : Symbol(RecursivePartial, Decl(recursiveMappedTypeAssignability.ts, 31, 2))
75+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 27, 12))
76+
77+
// ...
78+
})(arg);
79+
>arg : Symbol(arg, Decl(recursiveMappedTypeAssignability.ts, 27, 15))
80+
81+
};
82+
83+
type RecursivePartial<T> = {
84+
>RecursivePartial : Symbol(RecursivePartial, Decl(recursiveMappedTypeAssignability.ts, 31, 2))
85+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 33, 22))
86+
87+
[P in keyof T]?: RecursivePartial<T[P]>;
88+
>P : Symbol(P, Decl(recursiveMappedTypeAssignability.ts, 34, 5))
89+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 33, 22))
90+
>RecursivePartial : Symbol(RecursivePartial, Decl(recursiveMappedTypeAssignability.ts, 31, 2))
91+
>T : Symbol(T, Decl(recursiveMappedTypeAssignability.ts, 33, 22))
92+
>P : Symbol(P, Decl(recursiveMappedTypeAssignability.ts, 34, 5))
93+
94+
};
95+
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
=== tests/cases/compiler/recursiveMappedTypeAssignability.ts ===
2+
// type D<U> = { [P in keyof U]: D<U[P]> };
3+
// <T>(t: T, dt: D<T>) => { dt = t };
4+
// type DR<U> = { readonly [P in keyof U]: DR<U[P]> };
5+
// <T>(t: T, dt: DR<T>) => { dt = t };
6+
// type DP<U> = { [P in keyof U]?: DP<U[P]> };
7+
// <T>(t: T, dt: DP<T>) => { dt = t };
8+
// type DAP<U> = { [P in keyof U]?: DAP<U[P]> & U[P] };
9+
// <T>(t: T, dt: DAP<T>) => { dt = t };
10+
11+
// #21592
12+
// doesn't work because aliasSymbol isn't set on the literal type
13+
// since it's not top-level -- the union is.
14+
type SafeAny<T> = {
15+
>SafeAny : SafeAny<T>
16+
>T : T
17+
18+
[K in keyof T]?: SafeAny<T[K]>
19+
>K : K
20+
>T : T
21+
>SafeAny : SafeAny<T>
22+
>T : T
23+
>K : K
24+
25+
} | boolean | number | string | symbol | null | undefined
26+
>null : null
27+
28+
type DataValidator<T> = {
29+
>DataValidator : DataValidator<T>
30+
>T : T
31+
32+
[K in keyof T]?: (v: SafeAny<T[K]>) => v is T[K]
33+
>K : K
34+
>T : T
35+
>v : SafeAny<T[K]>
36+
>SafeAny : SafeAny<T>
37+
>T : T
38+
>K : K
39+
>v : any
40+
>T : T
41+
>K : K
42+
}
43+
44+
// modified repro with top-level mapped type, which works
45+
// because the literal type has aliasSymbol set
46+
type SafeAny2<T> = {
47+
>SafeAny2 : SafeAny2<T>
48+
>T : T
49+
50+
[K in keyof T]?: SafeAny2<T[K]>
51+
>K : K
52+
>T : T
53+
>SafeAny2 : SafeAny2<T>
54+
>T : T
55+
>K : K
56+
}
57+
<T>(t: T, sat: SafeAny2<T>) => { sat = t }
58+
><T>(t: T, sat: SafeAny2<T>) => { sat = t } : <T>(t: T, sat: SafeAny2<T>) => void
59+
>T : T
60+
>t : T
61+
>T : T
62+
>sat : SafeAny2<T>
63+
>SafeAny2 : SafeAny2<T>
64+
>T : T
65+
>sat = t : T
66+
>sat : SafeAny2<T>
67+
>t : T
68+
69+
70+
const fn = <T>(arg: T) => {
71+
>fn : <T>(arg: T) => void
72+
><T>(arg: T) => { ((arg2: RecursivePartial<T>) => { // ... })(arg);} : <T>(arg: T) => void
73+
>T : T
74+
>arg : T
75+
>T : T
76+
77+
((arg2: RecursivePartial<T>) => {
78+
>((arg2: RecursivePartial<T>) => { // ... })(arg) : void
79+
>((arg2: RecursivePartial<T>) => { // ... }) : (arg2: RecursivePartial<T>) => void
80+
>(arg2: RecursivePartial<T>) => { // ... } : (arg2: RecursivePartial<T>) => void
81+
>arg2 : RecursivePartial<T>
82+
>RecursivePartial : RecursivePartial<T>
83+
>T : T
84+
85+
// ...
86+
})(arg);
87+
>arg : T
88+
89+
};
90+
91+
type RecursivePartial<T> = {
92+
>RecursivePartial : RecursivePartial<T>
93+
>T : T
94+
95+
[P in keyof T]?: RecursivePartial<T[P]>;
96+
>P : P
97+
>T : T
98+
>RecursivePartial : RecursivePartial<T>
99+
>T : T
100+
>P : P
101+
102+
};
103+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// @strict: true
2+
// type D<U> = { [P in keyof U]: D<U[P]> };
3+
// <T>(t: T, dt: D<T>) => { dt = t };
4+
// type DR<U> = { readonly [P in keyof U]: DR<U[P]> };
5+
// <T>(t: T, dt: DR<T>) => { dt = t };
6+
// type DP<U> = { [P in keyof U]?: DP<U[P]> };
7+
// <T>(t: T, dt: DP<T>) => { dt = t };
8+
// type DAP<U> = { [P in keyof U]?: DAP<U[P]> & U[P] };
9+
// <T>(t: T, dt: DAP<T>) => { dt = t };
10+
11+
// #21592
12+
// doesn't work because aliasSymbol isn't set on the literal type
13+
// since it's not top-level -- the union is.
14+
type SafeAny<T> = {
15+
[K in keyof T]?: SafeAny<T[K]>
16+
} | boolean | number | string | symbol | null | undefined
17+
type DataValidator<T> = {
18+
[K in keyof T]?: (v: SafeAny<T[K]>) => v is T[K]
19+
}
20+
21+
// modified repro with top-level mapped type, which works
22+
// because the literal type has aliasSymbol set
23+
type SafeAny2<T> = {
24+
[K in keyof T]?: SafeAny2<T[K]>
25+
}
26+
<T>(t: T, sat: SafeAny2<T>) => { sat = t }
27+
28+
29+
const fn = <T>(arg: T) => {
30+
((arg2: RecursivePartial<T>) => {
31+
// ...
32+
})(arg);
33+
};
34+
35+
type RecursivePartial<T> = {
36+
[P in keyof T]?: RecursivePartial<T[P]>;
37+
};

0 commit comments

Comments
 (0)