Skip to content

Better detect when typical nondistributive conditionals need to be deferred... #42248

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
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
29 changes: 23 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14567,16 +14567,33 @@ namespace ts {
return type;
}

function isTypicalNondistributiveConditional(root: ConditionalRoot) {
return !root.isDistributive
&& root.node.checkType.kind === SyntaxKind.TupleType
&& length((root.node.checkType as TupleTypeNode).elements) === 1
&& root.node.extendsType.kind === SyntaxKind.TupleType
&& length((root.node.extendsType as TupleTypeNode).elements) === 1;
}

/**
* We syntactually check for common nondistributive conditional shapes and unwrap them into
* the intended comparison - we do this so we can check if the unwrapped types are generic or
* not and appropriately defer condition calculation
*/
function unwrapNondistributiveConditionalTuple(root: ConditionalRoot, type: Type) {
return isTypicalNondistributiveConditional(root) && isTupleType(type) ? getTypeArguments(type)[0] : type;
}

function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
let result;
let extraTypes: Type[] | undefined;
// We loop here for an immediately nested conditional type in the false position, effectively treating
// types of the form 'A extends B ? X : C extends D ? Y : E extends F ? Z : ...' as a single construct for
// purposes of resolution. This means such types aren't subject to the instatiation depth limiter.
while (true) {
const checkType = instantiateType(root.checkType, mapper);
const checkType = instantiateType(unwrapNondistributiveConditionalTuple(root, root.checkType), mapper);
const checkTypeInstantiable = isGenericObjectType(checkType) || isGenericIndexType(checkType);
const extendsType = instantiateType(root.extendsType, mapper);
const extendsType = instantiateType(unwrapNondistributiveConditionalTuple(root, root.extendsType), mapper);
if (checkType === wildcardType || extendsType === wildcardType) {
return wildcardType;
}
Expand All @@ -14599,9 +14616,9 @@ namespace ts {
// types with type parameters mapped to the wildcard type, the most permissive instantiations
// possible (the wildcard type is assignable to and from all types). If those are not related,
// then no instantiations will be and we can just return the false branch type.
if (!(inferredExtendsType.flags & TypeFlags.AnyOrUnknown) && (checkType.flags & TypeFlags.Any || !isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType)))) {
if (!(inferredExtendsType.flags & TypeFlags.AnyOrUnknown) && ((checkType.flags & TypeFlags.Any && root.isDistributive) || !isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType)))) {
// Return union of trueType and falseType for 'any' since it matches anything
if (checkType.flags & TypeFlags.Any) {
if (checkType.flags & TypeFlags.Any && root.isDistributive) {
(extraTypes || (extraTypes = [])).push(instantiateType(getTypeFromTypeNode(root.node.trueType), combinedMapper || mapper));
}
// If falseType is an immediately nested conditional type that isn't distributive or has an
Expand Down Expand Up @@ -14630,8 +14647,8 @@ namespace ts {
// Return a deferred type for a check that is neither definitely true nor definitely false
result = <ConditionalType>createType(TypeFlags.Conditional);
result.root = root;
result.checkType = checkType;
result.extendsType = extendsType;
result.checkType = instantiateType(root.checkType, mapper);
result.extendsType = instantiateType(root.extendsType, mapper);
result.mapper = mapper;
result.combinedMapper = combinedMapper;
result.aliasSymbol = aliasSymbol || root.aliasSymbol;
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/inferTypes1.types
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ type T61<T> = infer A extends infer B ? infer C : infer D; // Error
>T61 : T61<T>

type T62<T> = U extends (infer U)[] ? U : U; // Error
>T62 : any
>T62 : unknown

type T63<T> = T extends (infer A extends infer B ? infer C : infer D) ? string : number;
>T63 : T63<T>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//// [recursiveConditionalEvaluationNonInfinite.ts]
type Test<T> = [T] extends [any[]] ? { array: Test<T[0]> } : { notArray: T };
declare const x: Test<number[]>;
const y: { array: { notArray: number } } = x; // Error
declare const a: Test<number>;
const b: { notArray: number } = a; // Works

//// [recursiveConditionalEvaluationNonInfinite.js]
var y = x; // Error
var b = a; // Works
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
=== tests/cases/compiler/recursiveConditionalEvaluationNonInfinite.ts ===
type Test<T> = [T] extends [any[]] ? { array: Test<T[0]> } : { notArray: T };
>Test : Symbol(Test, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 0))
>T : Symbol(T, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 10))
>T : Symbol(T, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 10))
>array : Symbol(array, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 38))
>Test : Symbol(Test, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 0))
>T : Symbol(T, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 10))
>notArray : Symbol(notArray, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 62))
>T : Symbol(T, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 10))

declare const x: Test<number[]>;
>x : Symbol(x, Decl(recursiveConditionalEvaluationNonInfinite.ts, 1, 13))
>Test : Symbol(Test, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 0))

const y: { array: { notArray: number } } = x; // Error
>y : Symbol(y, Decl(recursiveConditionalEvaluationNonInfinite.ts, 2, 5))
>array : Symbol(array, Decl(recursiveConditionalEvaluationNonInfinite.ts, 2, 10))
>notArray : Symbol(notArray, Decl(recursiveConditionalEvaluationNonInfinite.ts, 2, 19))
>x : Symbol(x, Decl(recursiveConditionalEvaluationNonInfinite.ts, 1, 13))

declare const a: Test<number>;
>a : Symbol(a, Decl(recursiveConditionalEvaluationNonInfinite.ts, 3, 13))
>Test : Symbol(Test, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 0))

const b: { notArray: number } = a; // Works
>b : Symbol(b, Decl(recursiveConditionalEvaluationNonInfinite.ts, 4, 5))
>notArray : Symbol(notArray, Decl(recursiveConditionalEvaluationNonInfinite.ts, 4, 10))
>a : Symbol(a, Decl(recursiveConditionalEvaluationNonInfinite.ts, 3, 13))

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
=== tests/cases/compiler/recursiveConditionalEvaluationNonInfinite.ts ===
type Test<T> = [T] extends [any[]] ? { array: Test<T[0]> } : { notArray: T };
>Test : Test<T>
>array : Test<T[0]>
>notArray : T

declare const x: Test<number[]>;
>x : { array: { notArray: number; }; }

const y: { array: { notArray: number } } = x; // Error
>y : { array: { notArray: number;}; }
>array : { notArray: number; }
>notArray : number
>x : { array: { notArray: number; }; }

declare const a: Test<number>;
>a : { notArray: number; }

const b: { notArray: number } = a; // Works
>b : { notArray: number; }
>notArray : number
>a : { notArray: number; }

2 changes: 1 addition & 1 deletion tests/baselines/reference/recursiveConditionalTypes.types
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ type TT3 = TupleOf<number, any>;
>TT3 : number[]

type TT4 = TupleOf<number, 100>; // Depth error
>TT4 : any
>TT4 : [any, ...any[]]

function f22<N extends number, M extends N>(tn: TupleOf<number, N>, tm: TupleOf<number, M>) {
>f22 : <N extends number, M extends N>(tn: TupleOf<number, N>, tm: TupleOf<number, M>) => void
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type Test<T> = [T] extends [any[]] ? { array: Test<T[0]> } : { notArray: T };
declare const x: Test<number[]>;
const y: { array: { notArray: number } } = x; // Error
declare const a: Test<number>;
const b: { notArray: number } = a; // Works