Skip to content

Commit c3dd845

Browse files
authored
Better detect when typical nondistributive conditionals need to be defered by unwrapping their check and extends types (microsoft#42248)
1 parent a276a6d commit c3dd845

7 files changed

+93
-8
lines changed

src/compiler/checker.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14567,16 +14567,33 @@ namespace ts {
1456714567
return type;
1456814568
}
1456914569

14570+
function isTypicalNondistributiveConditional(root: ConditionalRoot) {
14571+
return !root.isDistributive
14572+
&& root.node.checkType.kind === SyntaxKind.TupleType
14573+
&& length((root.node.checkType as TupleTypeNode).elements) === 1
14574+
&& root.node.extendsType.kind === SyntaxKind.TupleType
14575+
&& length((root.node.extendsType as TupleTypeNode).elements) === 1;
14576+
}
14577+
14578+
/**
14579+
* We syntactually check for common nondistributive conditional shapes and unwrap them into
14580+
* the intended comparison - we do this so we can check if the unwrapped types are generic or
14581+
* not and appropriately defer condition calculation
14582+
*/
14583+
function unwrapNondistributiveConditionalTuple(root: ConditionalRoot, type: Type) {
14584+
return isTypicalNondistributiveConditional(root) && isTupleType(type) ? getTypeArguments(type)[0] : type;
14585+
}
14586+
1457014587
function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
1457114588
let result;
1457214589
let extraTypes: Type[] | undefined;
1457314590
// We loop here for an immediately nested conditional type in the false position, effectively treating
1457414591
// types of the form 'A extends B ? X : C extends D ? Y : E extends F ? Z : ...' as a single construct for
1457514592
// purposes of resolution. This means such types aren't subject to the instatiation depth limiter.
1457614593
while (true) {
14577-
const checkType = instantiateType(root.checkType, mapper);
14594+
const checkType = instantiateType(unwrapNondistributiveConditionalTuple(root, root.checkType), mapper);
1457814595
const checkTypeInstantiable = isGenericObjectType(checkType) || isGenericIndexType(checkType);
14579-
const extendsType = instantiateType(root.extendsType, mapper);
14596+
const extendsType = instantiateType(unwrapNondistributiveConditionalTuple(root, root.extendsType), mapper);
1458014597
if (checkType === wildcardType || extendsType === wildcardType) {
1458114598
return wildcardType;
1458214599
}
@@ -14599,9 +14616,9 @@ namespace ts {
1459914616
// types with type parameters mapped to the wildcard type, the most permissive instantiations
1460014617
// possible (the wildcard type is assignable to and from all types). If those are not related,
1460114618
// then no instantiations will be and we can just return the false branch type.
14602-
if (!(inferredExtendsType.flags & TypeFlags.AnyOrUnknown) && (checkType.flags & TypeFlags.Any || !isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType)))) {
14619+
if (!(inferredExtendsType.flags & TypeFlags.AnyOrUnknown) && ((checkType.flags & TypeFlags.Any && root.isDistributive) || !isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType)))) {
1460314620
// Return union of trueType and falseType for 'any' since it matches anything
14604-
if (checkType.flags & TypeFlags.Any) {
14621+
if (checkType.flags & TypeFlags.Any && root.isDistributive) {
1460514622
(extraTypes || (extraTypes = [])).push(instantiateType(getTypeFromTypeNode(root.node.trueType), combinedMapper || mapper));
1460614623
}
1460714624
// If falseType is an immediately nested conditional type that isn't distributive or has an
@@ -14630,8 +14647,8 @@ namespace ts {
1463014647
// Return a deferred type for a check that is neither definitely true nor definitely false
1463114648
result = <ConditionalType>createType(TypeFlags.Conditional);
1463214649
result.root = root;
14633-
result.checkType = checkType;
14634-
result.extendsType = extendsType;
14650+
result.checkType = instantiateType(root.checkType, mapper);
14651+
result.extendsType = instantiateType(root.extendsType, mapper);
1463514652
result.mapper = mapper;
1463614653
result.combinedMapper = combinedMapper;
1463714654
result.aliasSymbol = aliasSymbol || root.aliasSymbol;

tests/baselines/reference/inferTypes1.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ type T61<T> = infer A extends infer B ? infer C : infer D; // Error
257257
>T61 : T61<T>
258258

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

262262
type T63<T> = T extends (infer A extends infer B ? infer C : infer D) ? string : number;
263263
>T63 : T63<T>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//// [recursiveConditionalEvaluationNonInfinite.ts]
2+
type Test<T> = [T] extends [any[]] ? { array: Test<T[0]> } : { notArray: T };
3+
declare const x: Test<number[]>;
4+
const y: { array: { notArray: number } } = x; // Error
5+
declare const a: Test<number>;
6+
const b: { notArray: number } = a; // Works
7+
8+
//// [recursiveConditionalEvaluationNonInfinite.js]
9+
var y = x; // Error
10+
var b = a; // Works
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
=== tests/cases/compiler/recursiveConditionalEvaluationNonInfinite.ts ===
2+
type Test<T> = [T] extends [any[]] ? { array: Test<T[0]> } : { notArray: T };
3+
>Test : Symbol(Test, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 0))
4+
>T : Symbol(T, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 10))
5+
>T : Symbol(T, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 10))
6+
>array : Symbol(array, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 38))
7+
>Test : Symbol(Test, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 0))
8+
>T : Symbol(T, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 10))
9+
>notArray : Symbol(notArray, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 62))
10+
>T : Symbol(T, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 10))
11+
12+
declare const x: Test<number[]>;
13+
>x : Symbol(x, Decl(recursiveConditionalEvaluationNonInfinite.ts, 1, 13))
14+
>Test : Symbol(Test, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 0))
15+
16+
const y: { array: { notArray: number } } = x; // Error
17+
>y : Symbol(y, Decl(recursiveConditionalEvaluationNonInfinite.ts, 2, 5))
18+
>array : Symbol(array, Decl(recursiveConditionalEvaluationNonInfinite.ts, 2, 10))
19+
>notArray : Symbol(notArray, Decl(recursiveConditionalEvaluationNonInfinite.ts, 2, 19))
20+
>x : Symbol(x, Decl(recursiveConditionalEvaluationNonInfinite.ts, 1, 13))
21+
22+
declare const a: Test<number>;
23+
>a : Symbol(a, Decl(recursiveConditionalEvaluationNonInfinite.ts, 3, 13))
24+
>Test : Symbol(Test, Decl(recursiveConditionalEvaluationNonInfinite.ts, 0, 0))
25+
26+
const b: { notArray: number } = a; // Works
27+
>b : Symbol(b, Decl(recursiveConditionalEvaluationNonInfinite.ts, 4, 5))
28+
>notArray : Symbol(notArray, Decl(recursiveConditionalEvaluationNonInfinite.ts, 4, 10))
29+
>a : Symbol(a, Decl(recursiveConditionalEvaluationNonInfinite.ts, 3, 13))
30+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
=== tests/cases/compiler/recursiveConditionalEvaluationNonInfinite.ts ===
2+
type Test<T> = [T] extends [any[]] ? { array: Test<T[0]> } : { notArray: T };
3+
>Test : Test<T>
4+
>array : Test<T[0]>
5+
>notArray : T
6+
7+
declare const x: Test<number[]>;
8+
>x : { array: { notArray: number; }; }
9+
10+
const y: { array: { notArray: number } } = x; // Error
11+
>y : { array: { notArray: number;}; }
12+
>array : { notArray: number; }
13+
>notArray : number
14+
>x : { array: { notArray: number; }; }
15+
16+
declare const a: Test<number>;
17+
>a : { notArray: number; }
18+
19+
const b: { notArray: number } = a; // Works
20+
>b : { notArray: number; }
21+
>notArray : number
22+
>a : { notArray: number; }
23+

tests/baselines/reference/recursiveConditionalTypes.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ type TT3 = TupleOf<number, any>;
105105
>TT3 : number[]
106106

107107
type TT4 = TupleOf<number, 100>; // Depth error
108-
>TT4 : any
108+
>TT4 : [any, ...any[]]
109109

110110
function f22<N extends number, M extends N>(tn: TupleOf<number, N>, tm: TupleOf<number, M>) {
111111
>f22 : <N extends number, M extends N>(tn: TupleOf<number, N>, tm: TupleOf<number, M>) => void
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type Test<T> = [T] extends [any[]] ? { array: Test<T[0]> } : { notArray: T };
2+
declare const x: Test<number[]>;
3+
const y: { array: { notArray: number } } = x; // Error
4+
declare const a: Test<number>;
5+
const b: { notArray: number } = a; // Works

0 commit comments

Comments
 (0)