Skip to content

Commit 0d2aeb7

Browse files
authored
Improve checks for infinitely expanding recursive conditional types (#46326)
* Improve checks for infinitely expanding recursive conditional types * Accept new baselines * Add regression tests * Remove 'export' modifier * Accept new baselines
1 parent 315b807 commit 0d2aeb7

8 files changed

+308
-48
lines changed

src/compiler/checker.ts

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19024,6 +19024,12 @@ namespace ts {
1902419024
}
1902519025
}
1902619026
else if (target.flags & TypeFlags.Conditional) {
19027+
// If we reach 10 levels of nesting for the same conditional type, assume it is an infinitely expanding recursive
19028+
// conditional type and bail out with a Ternary.Maybe result.
19029+
if (isDeeplyNestedType(target, targetStack, targetDepth, 10)) {
19030+
resetErrorInfo(saveErrorInfo);
19031+
return Ternary.Maybe;
19032+
}
1902719033
const c = target as ConditionalType;
1902819034
// Check if the conditional is always true or always false but still deferred for distribution purposes
1902919035
const skipTrue = !isTypeAssignableTo(getPermissiveInstantiation(c.checkType), getPermissiveInstantiation(c.extendsType));
@@ -19123,33 +19129,34 @@ namespace ts {
1912319129
}
1912419130
}
1912519131
else if (source.flags & TypeFlags.Conditional) {
19132+
// If we reach 10 levels of nesting for the same conditional type, assume it is an infinitely expanding recursive
19133+
// conditional type and bail out with a Ternary.Maybe result.
19134+
if (isDeeplyNestedType(source, sourceStack, sourceDepth, 10)) {
19135+
resetErrorInfo(saveErrorInfo);
19136+
return Ternary.Maybe;
19137+
}
1912619138
if (target.flags & TypeFlags.Conditional) {
19127-
// If one of the conditionals under comparison seems to be infinitely expanding, stop comparing it - back out, try
19128-
// the constraint, and failing that, give up trying to relate the two. This is the only way we can handle recursive conditional
19129-
// types, which might expand forever.
19130-
if (!isDeeplyNestedType(source, sourceStack, sourceDepth) && !isDeeplyNestedType(target, targetStack, targetDepth)) {
19131-
// Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if
19132-
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
19133-
// and Y1 is related to Y2.
19134-
const sourceParams = (source as ConditionalType).root.inferTypeParameters;
19135-
let sourceExtends = (source as ConditionalType).extendsType;
19136-
let mapper: TypeMapper | undefined;
19137-
if (sourceParams) {
19138-
// If the source has infer type parameters, we instantiate them in the context of the target
19139-
const ctx = createInferenceContext(sourceParams, /*signature*/ undefined, InferenceFlags.None, isRelatedToWorker);
19140-
inferTypes(ctx.inferences, (target as ConditionalType).extendsType, sourceExtends, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
19141-
sourceExtends = instantiateType(sourceExtends, ctx.mapper);
19142-
mapper = ctx.mapper;
19143-
}
19144-
if (isTypeIdenticalTo(sourceExtends, (target as ConditionalType).extendsType) &&
19145-
(isRelatedTo((source as ConditionalType).checkType, (target as ConditionalType).checkType, RecursionFlags.Both) || isRelatedTo((target as ConditionalType).checkType, (source as ConditionalType).checkType, RecursionFlags.Both))) {
19146-
if (result = isRelatedTo(instantiateType(getTrueTypeFromConditionalType(source as ConditionalType), mapper), getTrueTypeFromConditionalType(target as ConditionalType), RecursionFlags.Both, reportErrors)) {
19147-
result &= isRelatedTo(getFalseTypeFromConditionalType(source as ConditionalType), getFalseTypeFromConditionalType(target as ConditionalType), RecursionFlags.Both, reportErrors);
19148-
}
19149-
if (result) {
19150-
resetErrorInfo(saveErrorInfo);
19151-
return result;
19152-
}
19139+
// Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if
19140+
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
19141+
// and Y1 is related to Y2.
19142+
const sourceParams = (source as ConditionalType).root.inferTypeParameters;
19143+
let sourceExtends = (source as ConditionalType).extendsType;
19144+
let mapper: TypeMapper | undefined;
19145+
if (sourceParams) {
19146+
// If the source has infer type parameters, we instantiate them in the context of the target
19147+
const ctx = createInferenceContext(sourceParams, /*signature*/ undefined, InferenceFlags.None, isRelatedToWorker);
19148+
inferTypes(ctx.inferences, (target as ConditionalType).extendsType, sourceExtends, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
19149+
sourceExtends = instantiateType(sourceExtends, ctx.mapper);
19150+
mapper = ctx.mapper;
19151+
}
19152+
if (isTypeIdenticalTo(sourceExtends, (target as ConditionalType).extendsType) &&
19153+
(isRelatedTo((source as ConditionalType).checkType, (target as ConditionalType).checkType, RecursionFlags.Both) || isRelatedTo((target as ConditionalType).checkType, (source as ConditionalType).checkType, RecursionFlags.Both))) {
19154+
if (result = isRelatedTo(instantiateType(getTrueTypeFromConditionalType(source as ConditionalType), mapper), getTrueTypeFromConditionalType(target as ConditionalType), RecursionFlags.Both, reportErrors)) {
19155+
result &= isRelatedTo(getFalseTypeFromConditionalType(source as ConditionalType), getFalseTypeFromConditionalType(target as ConditionalType), RecursionFlags.Both, reportErrors);
19156+
}
19157+
if (result) {
19158+
resetErrorInfo(saveErrorInfo);
19159+
return result;
1915319160
}
1915419161
}
1915519162
}
@@ -19165,18 +19172,13 @@ namespace ts {
1916519172
}
1916619173
}
1916719174

19168-
19169-
// We'll repeatedly decompose source side conditionals if they're recursive - check if we've already recured on the constraint a lot and, if so, bail
19170-
// on the comparison.
19171-
if (!isDeeplyNestedType(source, sourceStack, sourceDepth)) {
19172-
// conditionals _can_ be related to one another via normal constraint, as, eg, `A extends B ? O : never` should be assignable to `O`
19173-
// when `O` is a conditional (`never` is trivially assignable to `O`, as is `O`!).
19174-
const defaultConstraint = getDefaultConstraintOfConditionalType(source as ConditionalType);
19175-
if (defaultConstraint) {
19176-
if (result = isRelatedTo(defaultConstraint, target, RecursionFlags.Source, reportErrors)) {
19177-
resetErrorInfo(saveErrorInfo);
19178-
return result;
19179-
}
19175+
// conditionals _can_ be related to one another via normal constraint, as, eg, `A extends B ? O : never` should be assignable to `O`
19176+
// when `O` is a conditional (`never` is trivially assignable to `O`, as is `O`!).
19177+
const defaultConstraint = getDefaultConstraintOfConditionalType(source as ConditionalType);
19178+
if (defaultConstraint) {
19179+
if (result = isRelatedTo(defaultConstraint, target, RecursionFlags.Source, reportErrors)) {
19180+
resetErrorInfo(saveErrorInfo);
19181+
return result;
1918019182
}
1918119183
}
1918219184
}
@@ -20322,14 +20324,14 @@ namespace ts {
2032220324
// `type A<T> = null extends T ? [A<NonNullable<T>>] : [T]`
2032320325
// has expanded into `[A<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>>>>>>]`
2032420326
// in such cases we need to terminate the expansion, and we do so here.
20325-
function isDeeplyNestedType(type: Type, stack: Type[], depth: number): boolean {
20326-
if (depth >= 5) {
20327+
function isDeeplyNestedType(type: Type, stack: Type[], depth: number, maxDepth = 5): boolean {
20328+
if (depth >= maxDepth) {
2032720329
const identity = getRecursionIdentity(type);
2032820330
let count = 0;
2032920331
for (let i = 0; i < depth; i++) {
2033020332
if (getRecursionIdentity(stack[i]) === identity) {
2033120333
count++;
20332-
if (count >= 5) {
20334+
if (count >= maxDepth) {
2033320335
return true;
2033420336
}
2033520337
}

tests/baselines/reference/genericConditionalConstrainedToUnknownNotAssignableToConcreteObject.errors.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ tests/cases/compiler/genericConditionalConstrainedToUnknownNotAssignableToConcre
88
Type 'ReturnType<T[string]>' is not assignable to type 'A'.
99
Type 'unknown' is not assignable to type 'A'.
1010
Type 'ReturnType<FunctionsObj<T>[string]>' is not assignable to type 'A'.
11-
Property 'x' is missing in type '{}' but required in type 'A'.
11+
Type 'unknown' is not assignable to type 'A'.
12+
Property 'x' is missing in type '{}' but required in type 'A'.
1213

1314

1415
==== tests/cases/compiler/genericConditionalConstrainedToUnknownNotAssignableToConcreteObject.ts (1 errors) ====
@@ -36,7 +37,8 @@ tests/cases/compiler/genericConditionalConstrainedToUnknownNotAssignableToConcre
3637
!!! error TS2322: Type 'ReturnType<T[string]>' is not assignable to type 'A'.
3738
!!! error TS2322: Type 'unknown' is not assignable to type 'A'.
3839
!!! error TS2322: Type 'ReturnType<FunctionsObj<T>[string]>' is not assignable to type 'A'.
39-
!!! error TS2322: Property 'x' is missing in type '{}' but required in type 'A'.
40+
!!! error TS2322: Type 'unknown' is not assignable to type 'A'.
41+
!!! error TS2322: Property 'x' is missing in type '{}' but required in type 'A'.
4042
!!! related TS2728 tests/cases/compiler/genericConditionalConstrainedToUnknownNotAssignableToConcreteObject.ts:1:15: 'x' is declared here.
4143
}
4244

tests/baselines/reference/infiniteConstraints.errors.txt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ tests/cases/compiler/infiniteConstraints.ts(4,37): error TS2536: Type '"val"' ca
22
tests/cases/compiler/infiniteConstraints.ts(31,43): error TS2322: Type 'Value<"dup">' is not assignable to type 'never'.
33
tests/cases/compiler/infiniteConstraints.ts(31,63): error TS2322: Type 'Value<"dup">' is not assignable to type 'never'.
44
tests/cases/compiler/infiniteConstraints.ts(36,71): error TS2536: Type '"foo"' cannot be used to index type 'T[keyof T]'.
5-
tests/cases/compiler/infiniteConstraints.ts(48,27): error TS2321: Excessive stack depth comparing types 'Conv<ExactExtract<U, T>, ExactExtract<U, T>>' and 'unknown[]'.
65

76

8-
==== tests/cases/compiler/infiniteConstraints.ts (5 errors) ====
7+
==== tests/cases/compiler/infiniteConstraints.ts (4 errors) ====
98
// Both of the following types trigger the recursion limiter in getImmediateBaseConstraint
109

1110
type T1<B extends { [K in keyof B]: Extract<B[Exclude<keyof B, K>], { val: string }>["val"] }> = B;
@@ -64,6 +63,4 @@ tests/cases/compiler/infiniteConstraints.ts(48,27): error TS2321: Excessive stac
6463

6564
type Conv<T, U = T> =
6665
{ 0: [T]; 1: Prepend<T, Conv<ExactExtract<U, T>>>;}[U extends T ? 0 : 1];
67-
~~~~~~~~~~~~~~~~~~~~~~~~
68-
!!! error TS2321: Excessive stack depth comparing types 'Conv<ExactExtract<U, T>, ExactExtract<U, T>>' and 'unknown[]'.
6966

tests/baselines/reference/recursiveConditionalTypes.errors.txt

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ tests/cases/compiler/recursiveConditionalTypes.ts(117,9): error TS2345: Argument
2323
Type '[string]' is not assignable to type 'Grow1<[number], T>'.
2424
Type '[string]' is not assignable to type '[number]'.
2525
Type 'string' is not assignable to type 'number'.
26+
tests/cases/compiler/recursiveConditionalTypes.ts(169,5): error TS2322: Type 'number' is not assignable to type 'Enumerate<T["length"]>'.
2627

2728

28-
==== tests/cases/compiler/recursiveConditionalTypes.ts (9 errors) ====
29+
==== tests/cases/compiler/recursiveConditionalTypes.ts (10 errors) ====
2930
// Awaiting promises
3031

3132
type __Awaited<T> =
@@ -198,4 +199,38 @@ tests/cases/compiler/recursiveConditionalTypes.ts(117,9): error TS2345: Argument
198199
type Helper<T> = T extends ParseSuccess<infer R> ? ParseSuccess<R> : null
199200

200201
type TP2 = ParseManyWhitespace2<" foo">;
202+
203+
// Repro from #46183
204+
205+
type NTuple<N extends number, Tup extends unknown[] = []> =
206+
Tup['length'] extends N ? Tup : NTuple<N, [...Tup, unknown]>;
207+
208+
type Add<A extends number, B extends number> =
209+
[...NTuple<A>, ...NTuple<B>]['length'];
210+
211+
let five: Add<2, 3>;
212+
213+
// Repro from #46316
214+
215+
type _PrependNextNum<A extends Array<unknown>> = A['length'] extends infer T
216+
? [T, ...A] extends [...infer X]
217+
? X
218+
: never
219+
: never;
220+
221+
type _Enumerate<A extends Array<unknown>, N extends number> = N extends A['length']
222+
? A
223+
: _Enumerate<_PrependNextNum<A>, N> & number;
224+
225+
type Enumerate<N extends number> = number extends N
226+
? number
227+
: _Enumerate<[], N> extends (infer E)[]
228+
? E
229+
: never;
230+
231+
function foo2<T extends unknown[]>(value: T): Enumerate<T['length']> {
232+
return value.length; // Error
233+
~~~~~~~~~~~~~~~~~~~~
234+
!!! error TS2322: Type 'number' is not assignable to type 'Enumerate<T["length"]>'.
235+
}
201236

tests/baselines/reference/recursiveConditionalTypes.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,38 @@ type ParseManyWhitespace2<S extends string> =
137137
type Helper<T> = T extends ParseSuccess<infer R> ? ParseSuccess<R> : null
138138

139139
type TP2 = ParseManyWhitespace2<" foo">;
140+
141+
// Repro from #46183
142+
143+
type NTuple<N extends number, Tup extends unknown[] = []> =
144+
Tup['length'] extends N ? Tup : NTuple<N, [...Tup, unknown]>;
145+
146+
type Add<A extends number, B extends number> =
147+
[...NTuple<A>, ...NTuple<B>]['length'];
148+
149+
let five: Add<2, 3>;
150+
151+
// Repro from #46316
152+
153+
type _PrependNextNum<A extends Array<unknown>> = A['length'] extends infer T
154+
? [T, ...A] extends [...infer X]
155+
? X
156+
: never
157+
: never;
158+
159+
type _Enumerate<A extends Array<unknown>, N extends number> = N extends A['length']
160+
? A
161+
: _Enumerate<_PrependNextNum<A>, N> & number;
162+
163+
type Enumerate<N extends number> = number extends N
164+
? number
165+
: _Enumerate<[], N> extends (infer E)[]
166+
? E
167+
: never;
168+
169+
function foo2<T extends unknown[]>(value: T): Enumerate<T['length']> {
170+
return value.length; // Error
171+
}
140172

141173

142174
//// [recursiveConditionalTypes.js]
@@ -169,6 +201,10 @@ function f20(x, y) {
169201
function f21(x, y) {
170202
f21(y, x); // Error
171203
}
204+
let five;
205+
function foo2(value) {
206+
return value.length; // Error
207+
}
172208

173209

174210
//// [recursiveConditionalTypes.d.ts]
@@ -244,3 +280,13 @@ declare type TP1 = ParseManyWhitespace<" foo">;
244280
declare type ParseManyWhitespace2<S extends string> = S extends ` ${infer R0}` ? Helper<ParseManyWhitespace2<R0>> : ParseSuccess<S>;
245281
declare type Helper<T> = T extends ParseSuccess<infer R> ? ParseSuccess<R> : null;
246282
declare type TP2 = ParseManyWhitespace2<" foo">;
283+
declare type NTuple<N extends number, Tup extends unknown[] = []> = Tup['length'] extends N ? Tup : NTuple<N, [...Tup, unknown]>;
284+
declare type Add<A extends number, B extends number> = [
285+
...NTuple<A>,
286+
...NTuple<B>
287+
]['length'];
288+
declare let five: Add<2, 3>;
289+
declare type _PrependNextNum<A extends Array<unknown>> = A['length'] extends infer T ? [T, ...A] extends [...infer X] ? X : never : never;
290+
declare type _Enumerate<A extends Array<unknown>, N extends number> = N extends A['length'] ? A : _Enumerate<_PrependNextNum<A>, N> & number;
291+
declare type Enumerate<N extends number> = number extends N ? number : _Enumerate<[], N> extends (infer E)[] ? E : never;
292+
declare function foo2<T extends unknown[]>(value: T): Enumerate<T['length']>;

0 commit comments

Comments
 (0)