Skip to content

Commit a3ee09d

Browse files
authored
Handle recursive type references up to a certain level of expansion in inference (#38011)
* Handle recursive type references up to a certain level of expansion in inference * Move object cases into the same conditional branch
1 parent d1ebf12 commit a3ee09d

5 files changed

+153
-5
lines changed

src/compiler/checker.ts

+36-5
Original file line numberDiff line numberDiff line change
@@ -18090,15 +18090,32 @@ namespace ts {
1809018090
// In addition, this will also detect when an indexed access has been chained off of 5 or more times (which is essentially
1809118091
// the dual of the structural comparison), and likewise mark the type as deeply nested, potentially adding false positives
1809218092
// for finite but deeply expanding indexed accesses (eg, for `Q[P1][P2][P3][P4][P5]`).
18093+
// It also detects when a recursive type reference has expanded 5 or more times, eg, if the true branch of
18094+
// `type A<T> = null extends T ? [A<NonNullable<T>>] : [T]`
18095+
// has expanded into `[A<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>>>>>>]`
18096+
// in such cases we need to terminate the expansion, and we do so here.
1809318097
function isDeeplyNestedType(type: Type, stack: Type[], depth: number): boolean {
1809418098
// We track all object types that have an associated symbol (representing the origin of the type)
18095-
if (depth >= 5 && type.flags & TypeFlags.Object && !isObjectOrArrayLiteralType(type)) {
18096-
const symbol = type.symbol;
18097-
if (symbol) {
18099+
if (depth >= 5 && type.flags & TypeFlags.Object) {
18100+
if (!isObjectOrArrayLiteralType(type)) {
18101+
const symbol = type.symbol;
18102+
if (symbol) {
18103+
let count = 0;
18104+
for (let i = 0; i < depth; i++) {
18105+
const t = stack[i];
18106+
if (t.flags & TypeFlags.Object && t.symbol === symbol) {
18107+
count++;
18108+
if (count >= 5) return true;
18109+
}
18110+
}
18111+
}
18112+
}
18113+
if (getObjectFlags(type) && ObjectFlags.Reference && !!(type as TypeReference).node) {
18114+
const root = (type as TypeReference).target;
1809818115
let count = 0;
1809918116
for (let i = 0; i < depth; i++) {
1810018117
const t = stack[i];
18101-
if (t.flags & TypeFlags.Object && t.symbol === symbol) {
18118+
if (getObjectFlags(t) && ObjectFlags.Reference && !!(t as TypeReference).node && (t as TypeReference).target === root) {
1810218119
count++;
1810318120
if (count >= 5) return true;
1810418121
}
@@ -19190,6 +19207,8 @@ namespace ts {
1919019207
let propagationType: Type;
1919119208
let inferencePriority = InferencePriority.MaxValue;
1919219209
let allowComplexConstraintInference = true;
19210+
let objectTypeComparisonDepth = 0;
19211+
const targetStack: Type[] = [];
1919319212
inferFromTypes(originalSource, originalTarget);
1919419213

1919519214
function inferFromTypes(source: Type, target: Type): void {
@@ -19623,15 +19642,27 @@ namespace ts {
1962319642
// its symbol with the instance side which would lead to false positives.
1962419643
const isNonConstructorObject = target.flags & TypeFlags.Object &&
1962519644
!(getObjectFlags(target) & ObjectFlags.Anonymous && target.symbol && target.symbol.flags & SymbolFlags.Class);
19626-
const symbolOrType = isNonConstructorObject ? isTupleType(target) ? target.target : target.symbol : undefined;
19645+
const symbolOrType = getObjectFlags(target) & ObjectFlags.Reference && (target as TypeReference).node ? getNormalizedType(target, /*writing*/ false) : isNonConstructorObject ? isTupleType(target) ? target.target : target.symbol : undefined;
1962719646
if (symbolOrType) {
1962819647
if (contains(symbolOrTypeStack, symbolOrType)) {
19648+
if (getObjectFlags(target) & ObjectFlags.Reference && (target as TypeReference).node) {
19649+
// Don't set the circularity flag for re-encountered recursive type references just because we're already exploring them
19650+
return;
19651+
}
19652+
inferencePriority = InferencePriority.Circularity;
19653+
return;
19654+
}
19655+
targetStack[objectTypeComparisonDepth] = target;
19656+
objectTypeComparisonDepth++;
19657+
if (isDeeplyNestedType(target, targetStack, objectTypeComparisonDepth)) {
1962919658
inferencePriority = InferencePriority.Circularity;
19659+
objectTypeComparisonDepth--;
1963019660
return;
1963119661
}
1963219662
(symbolOrTypeStack || (symbolOrTypeStack = [])).push(symbolOrType);
1963319663
inferFromObjectTypesWorker(source, target);
1963419664
symbolOrTypeStack.pop();
19665+
objectTypeComparisonDepth--;
1963519666
}
1963619667
else {
1963719668
inferFromObjectTypesWorker(source, target);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//// [selfReferencingTypeReferenceInference.ts]
2+
interface Box<T> {
3+
__: T
4+
}
5+
6+
type Recursive<T> =
7+
| T
8+
| Box<Recursive<T>>
9+
10+
type InferRecursive<T> = T extends Recursive<infer R> ? R : "never!"
11+
12+
// the type we are testing with
13+
type t1 = Box<string | Box<number | boolean>>
14+
15+
type t2 = InferRecursive<t1>
16+
type t3 = InferRecursive<Box<string | Box<number | boolean>>> // write t1 explicitly
17+
18+
// Why is t2 and t3 different??
19+
// They have same input type!
20+
21+
//// [selfReferencingTypeReferenceInference.js]
22+
// Why is t2 and t3 different??
23+
// They have same input type!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
=== tests/cases/compiler/selfReferencingTypeReferenceInference.ts ===
2+
interface Box<T> {
3+
>Box : Symbol(Box, Decl(selfReferencingTypeReferenceInference.ts, 0, 0))
4+
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 0, 14))
5+
6+
__: T
7+
>__ : Symbol(Box.__, Decl(selfReferencingTypeReferenceInference.ts, 0, 18))
8+
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 0, 14))
9+
}
10+
11+
type Recursive<T> =
12+
>Recursive : Symbol(Recursive, Decl(selfReferencingTypeReferenceInference.ts, 2, 1))
13+
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 4, 15))
14+
15+
| T
16+
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 4, 15))
17+
18+
| Box<Recursive<T>>
19+
>Box : Symbol(Box, Decl(selfReferencingTypeReferenceInference.ts, 0, 0))
20+
>Recursive : Symbol(Recursive, Decl(selfReferencingTypeReferenceInference.ts, 2, 1))
21+
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 4, 15))
22+
23+
type InferRecursive<T> = T extends Recursive<infer R> ? R : "never!"
24+
>InferRecursive : Symbol(InferRecursive, Decl(selfReferencingTypeReferenceInference.ts, 6, 23))
25+
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 8, 20))
26+
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 8, 20))
27+
>Recursive : Symbol(Recursive, Decl(selfReferencingTypeReferenceInference.ts, 2, 1))
28+
>R : Symbol(R, Decl(selfReferencingTypeReferenceInference.ts, 8, 50))
29+
>R : Symbol(R, Decl(selfReferencingTypeReferenceInference.ts, 8, 50))
30+
31+
// the type we are testing with
32+
type t1 = Box<string | Box<number | boolean>>
33+
>t1 : Symbol(t1, Decl(selfReferencingTypeReferenceInference.ts, 8, 68))
34+
>Box : Symbol(Box, Decl(selfReferencingTypeReferenceInference.ts, 0, 0))
35+
>Box : Symbol(Box, Decl(selfReferencingTypeReferenceInference.ts, 0, 0))
36+
37+
type t2 = InferRecursive<t1>
38+
>t2 : Symbol(t2, Decl(selfReferencingTypeReferenceInference.ts, 11, 45))
39+
>InferRecursive : Symbol(InferRecursive, Decl(selfReferencingTypeReferenceInference.ts, 6, 23))
40+
>t1 : Symbol(t1, Decl(selfReferencingTypeReferenceInference.ts, 8, 68))
41+
42+
type t3 = InferRecursive<Box<string | Box<number | boolean>>> // write t1 explicitly
43+
>t3 : Symbol(t3, Decl(selfReferencingTypeReferenceInference.ts, 13, 28))
44+
>InferRecursive : Symbol(InferRecursive, Decl(selfReferencingTypeReferenceInference.ts, 6, 23))
45+
>Box : Symbol(Box, Decl(selfReferencingTypeReferenceInference.ts, 0, 0))
46+
>Box : Symbol(Box, Decl(selfReferencingTypeReferenceInference.ts, 0, 0))
47+
48+
// Why is t2 and t3 different??
49+
// They have same input type!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
=== tests/cases/compiler/selfReferencingTypeReferenceInference.ts ===
2+
interface Box<T> {
3+
__: T
4+
>__ : T
5+
}
6+
7+
type Recursive<T> =
8+
>Recursive : Recursive<T>
9+
10+
| T
11+
| Box<Recursive<T>>
12+
13+
type InferRecursive<T> = T extends Recursive<infer R> ? R : "never!"
14+
>InferRecursive : InferRecursive<T>
15+
16+
// the type we are testing with
17+
type t1 = Box<string | Box<number | boolean>>
18+
>t1 : t1
19+
20+
type t2 = InferRecursive<t1>
21+
>t2 : string | number | boolean
22+
23+
type t3 = InferRecursive<Box<string | Box<number | boolean>>> // write t1 explicitly
24+
>t3 : string | number | boolean
25+
26+
// Why is t2 and t3 different??
27+
// They have same input type!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
interface Box<T> {
2+
__: T
3+
}
4+
5+
type Recursive<T> =
6+
| T
7+
| Box<Recursive<T>>
8+
9+
type InferRecursive<T> = T extends Recursive<infer R> ? R : "never!"
10+
11+
// the type we are testing with
12+
type t1 = Box<string | Box<number | boolean>>
13+
14+
type t2 = InferRecursive<t1>
15+
type t3 = InferRecursive<Box<string | Box<number | boolean>>> // write t1 explicitly
16+
17+
// Why is t2 and t3 different??
18+
// They have same input type!

0 commit comments

Comments
 (0)