Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
31 changes: 30 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17332,6 +17332,10 @@ namespace ts {
// In addition, this will also detect when an indexed access has been chained off of 5 or more times (which is essentially
// the dual of the structural comparison), and likewise mark the type as deeply nested, potentially adding false positives
// for finite but deeply expanding indexed accesses (eg, for `Q[P1][P2][P3][P4][P5]`).
// It also detects when a recursive type reference has expanded 5 or more times, eg, if the true branch of
// `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 {
// We track all object types that have an associated symbol (representing the origin of the type)
if (depth >= 5 && type.flags & TypeFlags.Object && !isObjectOrArrayLiteralType(type)) {
Expand All @@ -17358,6 +17362,17 @@ namespace ts {
}
}
}
if (depth >= 5 && getObjectFlags(type) && ObjectFlags.Reference && !!(type as TypeReference).node) {
const root = (type as TypeReference).target;
let count = 0;
for (let i = 0; i < depth; i++) {
const t = stack[i];
if (getObjectFlags(t) && ObjectFlags.Reference && !!(t as TypeReference).node && (t as TypeReference).target === root) {
count++;
if (count >= 5) return true;
}
}
}
return false;
}

Expand Down Expand Up @@ -18389,6 +18404,8 @@ namespace ts {
let propagationType: Type;
let inferencePriority = InferencePriority.MaxValue;
let allowComplexConstraintInference = true;
let objectTypeComparisonDepth = 0;
const targetStack: Type[] = [];
inferFromTypes(originalSource, originalTarget);

function inferFromTypes(source: Type, target: Type): void {
Expand Down Expand Up @@ -18822,15 +18839,27 @@ namespace ts {
// its symbol with the instance side which would lead to false positives.
const isNonConstructorObject = target.flags & TypeFlags.Object &&
!(getObjectFlags(target) & ObjectFlags.Anonymous && target.symbol && target.symbol.flags & SymbolFlags.Class);
const symbolOrType = isNonConstructorObject ? isTupleType(target) ? target.target : target.symbol : undefined;
const symbolOrType = getObjectFlags(target) & ObjectFlags.Reference && (target as TypeReference).node ? getNormalizedType(target, /*writing*/ false) : isNonConstructorObject ? isTupleType(target) ? target.target : target.symbol : undefined;
Copy link
Member Author

Choose a reason for hiding this comment

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

This line (and the bail without setting Circularity below) essentially "undoes" #33678 insofar as if I only made this change, #37982 would be fixed (as now we'd explore all recursive type references so long as their normalized forms aren't identical), however doing just that reintroduces the issue #33678 fixed, since in that case, we have something like type T = [A<NonNullable<T>>] which expands to [A<NonNullable<NonNullable<T>>>] and so on. As it expands, at no point are the types "identical" (notable future work: collapsing noop conditional types like repeated NonNullable!), in fact, all that's shared is that the type reference is sourced from the same .target and .node. So when it was eliminating the recursion based on .target alone. it was essentially saying "you can't infer to a 1-tuple while inferring to a 1-tuple, if that 1-tuple is marked as recursive" (which, while in line with how limited inference is for other symbol-based object types without #31633, is a regression in behavior for recursive type references), which, in turn, meant you couldn't infer to, say, the second Q reference of [Q<string>, [Q<boolean | number>]] if Q caused the containing tuples to be marked as potentially recursive!

All this to say: the change in behavior was a direct consequence of two things: #33678 limited recursive type reference inference, and recursive type references are made syntactically, so they way they are declared changes if they're marked as potentially recursive or not, resulting in the differences noted in #37982. So the bulk of this change is about improving #33678 to allow exploring multiple instances of the "same" recursive type reference, so that syntactic determination if the reference is possibly recursive or not cannot be observed (up to a limit).

if (symbolOrType) {
if (contains(symbolOrTypeStack, symbolOrType)) {
if (getObjectFlags(target) & ObjectFlags.Reference && (target as TypeReference).node) {
// Don't set the circularity flag for re-encountered recursive type references just because we're already exploring them
return;
}
inferencePriority = InferencePriority.Circularity;
return;
}
targetStack[objectTypeComparisonDepth] = target;
objectTypeComparisonDepth++;
if (isDeeplyNestedType(target, targetStack, objectTypeComparisonDepth)) {
inferencePriority = InferencePriority.Circularity;
objectTypeComparisonDepth--;
return;
}
(symbolOrTypeStack || (symbolOrTypeStack = [])).push(symbolOrType);
inferFromObjectTypesWorker(source, target);
symbolOrTypeStack.pop();
objectTypeComparisonDepth--;
}
else {
inferFromObjectTypesWorker(source, target);
Expand Down
23 changes: 23 additions & 0 deletions tests/baselines/reference/selfReferencingTypeReferenceInference.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//// [selfReferencingTypeReferenceInference.ts]
interface Box<T> {
__: T
}

type Recursive<T> =
| T
| Box<Recursive<T>>

type InferRecursive<T> = T extends Recursive<infer R> ? R : "never!"

// the type we are testing with
type t1 = Box<string | Box<number | boolean>>

type t2 = InferRecursive<t1>
type t3 = InferRecursive<Box<string | Box<number | boolean>>> // write t1 explicitly

// Why is t2 and t3 different??
// They have same input type!

//// [selfReferencingTypeReferenceInference.js]
// Why is t2 and t3 different??
// They have same input type!
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
=== tests/cases/compiler/selfReferencingTypeReferenceInference.ts ===
interface Box<T> {
>Box : Symbol(Box, Decl(selfReferencingTypeReferenceInference.ts, 0, 0))
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 0, 14))

__: T
>__ : Symbol(Box.__, Decl(selfReferencingTypeReferenceInference.ts, 0, 18))
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 0, 14))
}

type Recursive<T> =
>Recursive : Symbol(Recursive, Decl(selfReferencingTypeReferenceInference.ts, 2, 1))
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 4, 15))

| T
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 4, 15))

| Box<Recursive<T>>
>Box : Symbol(Box, Decl(selfReferencingTypeReferenceInference.ts, 0, 0))
>Recursive : Symbol(Recursive, Decl(selfReferencingTypeReferenceInference.ts, 2, 1))
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 4, 15))

type InferRecursive<T> = T extends Recursive<infer R> ? R : "never!"
>InferRecursive : Symbol(InferRecursive, Decl(selfReferencingTypeReferenceInference.ts, 6, 23))
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 8, 20))
>T : Symbol(T, Decl(selfReferencingTypeReferenceInference.ts, 8, 20))
>Recursive : Symbol(Recursive, Decl(selfReferencingTypeReferenceInference.ts, 2, 1))
>R : Symbol(R, Decl(selfReferencingTypeReferenceInference.ts, 8, 50))
>R : Symbol(R, Decl(selfReferencingTypeReferenceInference.ts, 8, 50))

// the type we are testing with
type t1 = Box<string | Box<number | boolean>>
>t1 : Symbol(t1, Decl(selfReferencingTypeReferenceInference.ts, 8, 68))
>Box : Symbol(Box, Decl(selfReferencingTypeReferenceInference.ts, 0, 0))
>Box : Symbol(Box, Decl(selfReferencingTypeReferenceInference.ts, 0, 0))

type t2 = InferRecursive<t1>
>t2 : Symbol(t2, Decl(selfReferencingTypeReferenceInference.ts, 11, 45))
>InferRecursive : Symbol(InferRecursive, Decl(selfReferencingTypeReferenceInference.ts, 6, 23))
>t1 : Symbol(t1, Decl(selfReferencingTypeReferenceInference.ts, 8, 68))

type t3 = InferRecursive<Box<string | Box<number | boolean>>> // write t1 explicitly
>t3 : Symbol(t3, Decl(selfReferencingTypeReferenceInference.ts, 13, 28))
>InferRecursive : Symbol(InferRecursive, Decl(selfReferencingTypeReferenceInference.ts, 6, 23))
>Box : Symbol(Box, Decl(selfReferencingTypeReferenceInference.ts, 0, 0))
>Box : Symbol(Box, Decl(selfReferencingTypeReferenceInference.ts, 0, 0))

// Why is t2 and t3 different??
// They have same input type!
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
=== tests/cases/compiler/selfReferencingTypeReferenceInference.ts ===
interface Box<T> {
__: T
>__ : T
}

type Recursive<T> =
>Recursive : Recursive<T>

| T
| Box<Recursive<T>>

type InferRecursive<T> = T extends Recursive<infer R> ? R : "never!"
>InferRecursive : InferRecursive<T>

// the type we are testing with
type t1 = Box<string | Box<number | boolean>>
>t1 : t1

type t2 = InferRecursive<t1>
>t2 : string | number | boolean

type t3 = InferRecursive<Box<string | Box<number | boolean>>> // write t1 explicitly
>t3 : string | number | boolean

// Why is t2 and t3 different??
// They have same input type!
18 changes: 18 additions & 0 deletions tests/cases/compiler/selfReferencingTypeReferenceInference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
interface Box<T> {
__: T
}

type Recursive<T> =
| T
| Box<Recursive<T>>

type InferRecursive<T> = T extends Recursive<infer R> ? R : "never!"

// the type we are testing with
type t1 = Box<string | Box<number | boolean>>

type t2 = InferRecursive<t1>
type t3 = InferRecursive<Box<string | Box<number | boolean>>> // write t1 explicitly

// Why is t2 and t3 different??
// They have same input type!