Skip to content

Commit 73317dc

Browse files
committed
Handle recursive type references up to a certain level of expansion in inference
1 parent deb5bac commit 73317dc

File tree

5 files changed

+147
-1
lines changed

5 files changed

+147
-1
lines changed

src/compiler/checker.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17332,6 +17332,10 @@ namespace ts {
1733217332
// In addition, this will also detect when an indexed access has been chained off of 5 or more times (which is essentially
1733317333
// the dual of the structural comparison), and likewise mark the type as deeply nested, potentially adding false positives
1733417334
// for finite but deeply expanding indexed accesses (eg, for `Q[P1][P2][P3][P4][P5]`).
17335+
// It also detects when a recursive type reference has expanded 5 or more times, eg, if the true branch of
17336+
// `type A<T> = null extends T ? [A<NonNullable<T>>] : [T]`
17337+
// has expanded into `[A<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>>>>>>]`
17338+
// in such cases we need to terminate the expansion, and we do so here.
1733517339
function isDeeplyNestedType(type: Type, stack: Type[], depth: number): boolean {
1733617340
// We track all object types that have an associated symbol (representing the origin of the type)
1733717341
if (depth >= 5 && type.flags & TypeFlags.Object && !isObjectOrArrayLiteralType(type)) {
@@ -17358,6 +17362,17 @@ namespace ts {
1735817362
}
1735917363
}
1736017364
}
17365+
if (depth >= 5 && getObjectFlags(type) && ObjectFlags.Reference && !!(type as TypeReference).node) {
17366+
const root = (type as TypeReference).target;
17367+
let count = 0;
17368+
for (let i = 0; i < depth; i++) {
17369+
const t = stack[i];
17370+
if (getObjectFlags(t) && ObjectFlags.Reference && !!(t as TypeReference).node && (t as TypeReference).target === root) {
17371+
count++;
17372+
if (count >= 5) return true;
17373+
}
17374+
}
17375+
}
1736117376
return false;
1736217377
}
1736317378

@@ -18389,6 +18404,8 @@ namespace ts {
1838918404
let propagationType: Type;
1839018405
let inferencePriority = InferencePriority.MaxValue;
1839118406
let allowComplexConstraintInference = true;
18407+
let objectTypeComparisonDepth = 0;
18408+
const targetStack: Type[] = [];
1839218409
inferFromTypes(originalSource, originalTarget);
1839318410

1839418411
function inferFromTypes(source: Type, target: Type): void {
@@ -18822,15 +18839,27 @@ namespace ts {
1882218839
// its symbol with the instance side which would lead to false positives.
1882318840
const isNonConstructorObject = target.flags & TypeFlags.Object &&
1882418841
!(getObjectFlags(target) & ObjectFlags.Anonymous && target.symbol && target.symbol.flags & SymbolFlags.Class);
18825-
const symbolOrType = isNonConstructorObject ? isTupleType(target) ? target.target : target.symbol : undefined;
18842+
const symbolOrType = getObjectFlags(target) & ObjectFlags.Reference && (target as TypeReference).node ? getNormalizedType(target, /*writing*/ false) : isNonConstructorObject ? isTupleType(target) ? target.target : target.symbol : undefined;
1882618843
if (symbolOrType) {
1882718844
if (contains(symbolOrTypeStack, symbolOrType)) {
18845+
if (getObjectFlags(target) & ObjectFlags.Reference && (target as TypeReference).node) {
18846+
// Don't set the circularity flag for re-encountered recursive type references just because we're already exploring them
18847+
return;
18848+
}
18849+
inferencePriority = InferencePriority.Circularity;
18850+
return;
18851+
}
18852+
targetStack[objectTypeComparisonDepth] = target;
18853+
objectTypeComparisonDepth++;
18854+
if (isDeeplyNestedType(target, targetStack, objectTypeComparisonDepth)) {
1882818855
inferencePriority = InferencePriority.Circularity;
18856+
objectTypeComparisonDepth--;
1882918857
return;
1883018858
}
1883118859
(symbolOrTypeStack || (symbolOrTypeStack = [])).push(symbolOrType);
1883218860
inferFromObjectTypesWorker(source, target);
1883318861
symbolOrTypeStack.pop();
18862+
objectTypeComparisonDepth--;
1883418863
}
1883518864
else {
1883618865
inferFromObjectTypesWorker(source, target);
Lines changed: 23 additions & 0 deletions
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!
Lines changed: 49 additions & 0 deletions
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!
Lines changed: 27 additions & 0 deletions
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!
Lines changed: 18 additions & 0 deletions
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)