Skip to content

Improve checks for infinitely expanding recursive conditional types #46326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 43 additions & 41 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19023,6 +19023,12 @@ namespace ts {
}
}
else if (target.flags & TypeFlags.Conditional) {
// If we reach 10 levels of nesting for the same conditional type, assume it is an infinitely expanding recursive
// conditional type and bail out with a Ternary.Maybe result.
if (isDeeplyNestedType(target, targetStack, targetDepth, 10)) {
resetErrorInfo(saveErrorInfo);
return Ternary.Maybe;
}
const c = target as ConditionalType;
// Check if the conditional is always true or always false but still deferred for distribution purposes
const skipTrue = !isTypeAssignableTo(getPermissiveInstantiation(c.checkType), getPermissiveInstantiation(c.extendsType));
Expand Down Expand Up @@ -19122,33 +19128,34 @@ namespace ts {
}
}
else if (source.flags & TypeFlags.Conditional) {
// If we reach 10 levels of nesting for the same conditional type, assume it is an infinitely expanding recursive
// conditional type and bail out with a Ternary.Maybe result.
if (isDeeplyNestedType(source, sourceStack, sourceDepth, 10)) {
resetErrorInfo(saveErrorInfo);
return Ternary.Maybe;
}
if (target.flags & TypeFlags.Conditional) {
// If one of the conditionals under comparison seems to be infinitely expanding, stop comparing it - back out, try
// the constraint, and failing that, give up trying to relate the two. This is the only way we can handle recursive conditional
// types, which might expand forever.
if (!isDeeplyNestedType(source, sourceStack, sourceDepth) && !isDeeplyNestedType(target, targetStack, targetDepth)) {
// Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
// and Y1 is related to Y2.
const sourceParams = (source as ConditionalType).root.inferTypeParameters;
let sourceExtends = (source as ConditionalType).extendsType;
let mapper: TypeMapper | undefined;
if (sourceParams) {
// If the source has infer type parameters, we instantiate them in the context of the target
const ctx = createInferenceContext(sourceParams, /*signature*/ undefined, InferenceFlags.None, isRelatedToWorker);
inferTypes(ctx.inferences, (target as ConditionalType).extendsType, sourceExtends, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
sourceExtends = instantiateType(sourceExtends, ctx.mapper);
mapper = ctx.mapper;
}
if (isTypeIdenticalTo(sourceExtends, (target as ConditionalType).extendsType) &&
(isRelatedTo((source as ConditionalType).checkType, (target as ConditionalType).checkType, RecursionFlags.Both) || isRelatedTo((target as ConditionalType).checkType, (source as ConditionalType).checkType, RecursionFlags.Both))) {
if (result = isRelatedTo(instantiateType(getTrueTypeFromConditionalType(source as ConditionalType), mapper), getTrueTypeFromConditionalType(target as ConditionalType), RecursionFlags.Both, reportErrors)) {
result &= isRelatedTo(getFalseTypeFromConditionalType(source as ConditionalType), getFalseTypeFromConditionalType(target as ConditionalType), RecursionFlags.Both, reportErrors);
}
if (result) {
resetErrorInfo(saveErrorInfo);
return result;
}
// Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
// and Y1 is related to Y2.
const sourceParams = (source as ConditionalType).root.inferTypeParameters;
let sourceExtends = (source as ConditionalType).extendsType;
let mapper: TypeMapper | undefined;
if (sourceParams) {
// If the source has infer type parameters, we instantiate them in the context of the target
const ctx = createInferenceContext(sourceParams, /*signature*/ undefined, InferenceFlags.None, isRelatedToWorker);
inferTypes(ctx.inferences, (target as ConditionalType).extendsType, sourceExtends, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
sourceExtends = instantiateType(sourceExtends, ctx.mapper);
mapper = ctx.mapper;
}
if (isTypeIdenticalTo(sourceExtends, (target as ConditionalType).extendsType) &&
(isRelatedTo((source as ConditionalType).checkType, (target as ConditionalType).checkType, RecursionFlags.Both) || isRelatedTo((target as ConditionalType).checkType, (source as ConditionalType).checkType, RecursionFlags.Both))) {
if (result = isRelatedTo(instantiateType(getTrueTypeFromConditionalType(source as ConditionalType), mapper), getTrueTypeFromConditionalType(target as ConditionalType), RecursionFlags.Both, reportErrors)) {
result &= isRelatedTo(getFalseTypeFromConditionalType(source as ConditionalType), getFalseTypeFromConditionalType(target as ConditionalType), RecursionFlags.Both, reportErrors);
}
if (result) {
resetErrorInfo(saveErrorInfo);
return result;
}
}
}
Expand All @@ -19164,18 +19171,13 @@ namespace ts {
}
}


// 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
// on the comparison.
if (!isDeeplyNestedType(source, sourceStack, sourceDepth)) {
// conditionals _can_ be related to one another via normal constraint, as, eg, `A extends B ? O : never` should be assignable to `O`
// when `O` is a conditional (`never` is trivially assignable to `O`, as is `O`!).
const defaultConstraint = getDefaultConstraintOfConditionalType(source as ConditionalType);
if (defaultConstraint) {
if (result = isRelatedTo(defaultConstraint, target, RecursionFlags.Source, reportErrors)) {
resetErrorInfo(saveErrorInfo);
return result;
}
// conditionals _can_ be related to one another via normal constraint, as, eg, `A extends B ? O : never` should be assignable to `O`
// when `O` is a conditional (`never` is trivially assignable to `O`, as is `O`!).
const defaultConstraint = getDefaultConstraintOfConditionalType(source as ConditionalType);
if (defaultConstraint) {
if (result = isRelatedTo(defaultConstraint, target, RecursionFlags.Source, reportErrors)) {
resetErrorInfo(saveErrorInfo);
return result;
}
}
}
Expand Down Expand Up @@ -20321,14 +20323,14 @@ namespace ts {
// `type A<T> = null extends T ? [A<NonNullable<T>>] : [T]`
// has expanded into `[A<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>>>>>>]`
// in such cases we need to terminate the expansion, and we do so here.
function isDeeplyNestedType(type: Type, stack: Type[], depth: number): boolean {
if (depth >= 5) {
function isDeeplyNestedType(type: Type, stack: Type[], depth: number, maxDepth = 5): boolean {
if (depth >= maxDepth) {
const identity = getRecursionIdentity(type);
let count = 0;
for (let i = 0; i < depth; i++) {
if (getRecursionIdentity(stack[i]) === identity) {
count++;
if (count >= 5) {
if (count >= maxDepth) {
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ tests/cases/compiler/genericConditionalConstrainedToUnknownNotAssignableToConcre
Type 'ReturnType<T[string]>' is not assignable to type 'A'.
Type 'unknown' is not assignable to type 'A'.
Type 'ReturnType<FunctionsObj<T>[string]>' is not assignable to type 'A'.
Property 'x' is missing in type '{}' but required in type 'A'.
Type 'unknown' is not assignable to type 'A'.
Property 'x' is missing in type '{}' but required in type 'A'.


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

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


==== tests/cases/compiler/infiniteConstraints.ts (5 errors) ====
==== tests/cases/compiler/infiniteConstraints.ts (4 errors) ====
// Both of the following types trigger the recursion limiter in getImmediateBaseConstraint

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

type Conv<T, U = T> =
{ 0: [T]; 1: Prepend<T, Conv<ExactExtract<U, T>>>;}[U extends T ? 0 : 1];
~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2321: Excessive stack depth comparing types 'Conv<ExactExtract<U, T>, ExactExtract<U, T>>' and 'unknown[]'.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test has flip-flopped weather or not there's an error here so many times. I guess that's what it's here for, though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but it's a good change. The excessive stack depth error is basically a panic bail out, in this case triggered by a missing check for infinite conditional type recursion on the target side.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I just chuckle because when this test was first added it had this error, then it got removed by another change, then it got added back, and now it's being removed again. This test is surprisingly good at capturing how strict we are about comparing infinite types, even if that's not what it was meant to test (it was meant to test a crash).


37 changes: 36 additions & 1 deletion tests/baselines/reference/recursiveConditionalTypes.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ tests/cases/compiler/recursiveConditionalTypes.ts(117,9): error TS2345: Argument
Type '[string]' is not assignable to type 'Grow1<[number], T>'.
Type '[string]' is not assignable to type '[number]'.
Type 'string' is not assignable to type 'number'.
tests/cases/compiler/recursiveConditionalTypes.ts(169,5): error TS2322: Type 'number' is not assignable to type 'Enumerate<T["length"]>'.


==== tests/cases/compiler/recursiveConditionalTypes.ts (9 errors) ====
==== tests/cases/compiler/recursiveConditionalTypes.ts (10 errors) ====
// Awaiting promises

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

type TP2 = ParseManyWhitespace2<" foo">;

// Repro from #46183

type NTuple<N extends number, Tup extends unknown[] = []> =
Tup['length'] extends N ? Tup : NTuple<N, [...Tup, unknown]>;

type Add<A extends number, B extends number> =
[...NTuple<A>, ...NTuple<B>]['length'];

let five: Add<2, 3>;

// Repro from #46316

type _PrependNextNum<A extends Array<unknown>> = A['length'] extends infer T
? [T, ...A] extends [...infer X]
? X
: never
: never;

type _Enumerate<A extends Array<unknown>, N extends number> = N extends A['length']
? A
: _Enumerate<_PrependNextNum<A>, N> & number;

type Enumerate<N extends number> = number extends N
? number
: _Enumerate<[], N> extends (infer E)[]
? E
: never;

function foo2<T extends unknown[]>(value: T): Enumerate<T['length']> {
return value.length; // Error
~~~~~~~~~~~~~~~~~~~~
!!! error TS2322: Type 'number' is not assignable to type 'Enumerate<T["length"]>'.
}

46 changes: 46 additions & 0 deletions tests/baselines/reference/recursiveConditionalTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,38 @@ type ParseManyWhitespace2<S extends string> =
type Helper<T> = T extends ParseSuccess<infer R> ? ParseSuccess<R> : null

type TP2 = ParseManyWhitespace2<" foo">;

// Repro from #46183

type NTuple<N extends number, Tup extends unknown[] = []> =
Tup['length'] extends N ? Tup : NTuple<N, [...Tup, unknown]>;

type Add<A extends number, B extends number> =
[...NTuple<A>, ...NTuple<B>]['length'];

let five: Add<2, 3>;

// Repro from #46316

type _PrependNextNum<A extends Array<unknown>> = A['length'] extends infer T
? [T, ...A] extends [...infer X]
? X
: never
: never;

type _Enumerate<A extends Array<unknown>, N extends number> = N extends A['length']
? A
: _Enumerate<_PrependNextNum<A>, N> & number;

type Enumerate<N extends number> = number extends N
? number
: _Enumerate<[], N> extends (infer E)[]
? E
: never;

function foo2<T extends unknown[]>(value: T): Enumerate<T['length']> {
return value.length; // Error
}


//// [recursiveConditionalTypes.js]
Expand Down Expand Up @@ -169,6 +201,10 @@ function f20(x, y) {
function f21(x, y) {
f21(y, x); // Error
}
let five;
function foo2(value) {
return value.length; // Error
}


//// [recursiveConditionalTypes.d.ts]
Expand Down Expand Up @@ -244,3 +280,13 @@ declare type TP1 = ParseManyWhitespace<" foo">;
declare type ParseManyWhitespace2<S extends string> = S extends ` ${infer R0}` ? Helper<ParseManyWhitespace2<R0>> : ParseSuccess<S>;
declare type Helper<T> = T extends ParseSuccess<infer R> ? ParseSuccess<R> : null;
declare type TP2 = ParseManyWhitespace2<" foo">;
declare type NTuple<N extends number, Tup extends unknown[] = []> = Tup['length'] extends N ? Tup : NTuple<N, [...Tup, unknown]>;
declare type Add<A extends number, B extends number> = [
...NTuple<A>,
...NTuple<B>
]['length'];
declare let five: Add<2, 3>;
declare type _PrependNextNum<A extends Array<unknown>> = A['length'] extends infer T ? [T, ...A] extends [...infer X] ? X : never : never;
declare type _Enumerate<A extends Array<unknown>, N extends number> = N extends A['length'] ? A : _Enumerate<_PrependNextNum<A>, N> & number;
declare type Enumerate<N extends number> = number extends N ? number : _Enumerate<[], N> extends (infer E)[] ? E : never;
declare function foo2<T extends unknown[]>(value: T): Enumerate<T['length']>;
Loading