Skip to content

Commit 9120497

Browse files
Flatten immediately nested conditional types in the false position (#36583)
* Flatten immediately nested conditional types in the false position * Add test * Accept new baselines * Handle nested distributive types with different checkType * Allow deeply nested immediately resolving conditionals without any syntactic requirements or implementation contortions Extract logic into function Co-authored-by: Wesley Wigham <wewigham@microsoft.com>
1 parent 2458c8a commit 9120497

File tree

5 files changed

+737
-58
lines changed

5 files changed

+737
-58
lines changed

src/compiler/checker.ts

Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12889,65 +12889,82 @@ namespace ts {
1288912889
}
1289012890

1289112891
function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined): Type {
12892-
const checkType = instantiateType(root.checkType, mapper);
12893-
const extendsType = instantiateType(root.extendsType, mapper);
12894-
if (checkType === wildcardType || extendsType === wildcardType) {
12895-
return wildcardType;
12892+
let result;
12893+
let extraTypes: Type[] | undefined;
12894+
// We loop here for an immediately nested conditional type in the false position, effectively treating
12895+
// types of the form 'A extends B ? X : C extends D ? Y : E extends F ? Z : ...' as a single construct for
12896+
// purposes of resolution. This means such types aren't subject to the instatiation depth limiter.
12897+
while (true) {
12898+
const checkType = instantiateType(root.checkType, mapper);
12899+
const checkTypeInstantiable = isGenericObjectType(checkType) || isGenericIndexType(checkType);
12900+
const extendsType = instantiateType(root.extendsType, mapper);
12901+
if (checkType === wildcardType || extendsType === wildcardType) {
12902+
return wildcardType;
12903+
}
12904+
let combinedMapper: TypeMapper | undefined;
12905+
if (root.inferTypeParameters) {
12906+
const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None);
12907+
// We skip inference of the possible `infer` types unles the `extendsType` _is_ an infer type
12908+
// if it was, it's trivial to say that extendsType = checkType, however such a pattern is used to
12909+
// "reset" the type being build up during constraint calculation and avoid making an apparently "infinite" constraint
12910+
// so in those cases we refain from performing inference and retain the uninfered type parameter
12911+
if (!checkTypeInstantiable || !some(root.inferTypeParameters, t => t === extendsType)) {
12912+
// We don't want inferences from constraints as they may cause us to eagerly resolve the
12913+
// conditional type instead of deferring resolution. Also, we always want strict function
12914+
// types rules (i.e. proper contravariance) for inferences.
12915+
inferTypes(context.inferences, checkType, extendsType, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
12916+
}
12917+
combinedMapper = mergeTypeMappers(mapper, context.mapper);
12918+
}
12919+
// Instantiate the extends type including inferences for 'infer T' type parameters
12920+
const inferredExtendsType = combinedMapper ? instantiateType(root.extendsType, combinedMapper) : extendsType;
12921+
// We attempt to resolve the conditional type only when the check and extends types are non-generic
12922+
if (!checkTypeInstantiable && !isGenericObjectType(inferredExtendsType) && !isGenericIndexType(inferredExtendsType)) {
12923+
// Return falseType for a definitely false extends check. We check an instantiations of the two
12924+
// types with type parameters mapped to the wildcard type, the most permissive instantiations
12925+
// possible (the wildcard type is assignable to and from all types). If those are not related,
12926+
// then no instantiations will be and we can just return the false branch type.
12927+
if (!(inferredExtendsType.flags & TypeFlags.AnyOrUnknown) && (checkType.flags & TypeFlags.Any || !isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType)))) {
12928+
// Return union of trueType and falseType for 'any' since it matches anything
12929+
if (checkType.flags & TypeFlags.Any) {
12930+
(extraTypes || (extraTypes = [])).push(instantiateTypeWithoutDepthIncrease(root.trueType, combinedMapper || mapper));
12931+
}
12932+
// If falseType is an immediately nested conditional type that isn't distributive or has an
12933+
// identical checkType, switch to that type and loop.
12934+
const falseType = root.falseType;
12935+
if (falseType.flags & TypeFlags.Conditional) {
12936+
const newRoot = (<ConditionalType>falseType).root;
12937+
if (newRoot.node.parent === root.node && (!newRoot.isDistributive || newRoot.checkType === root.checkType)) {
12938+
root = newRoot;
12939+
continue;
12940+
}
12941+
}
12942+
result = instantiateTypeWithoutDepthIncrease(falseType, mapper);
12943+
break;
12944+
}
12945+
// Return trueType for a definitely true extends check. We check instantiations of the two
12946+
// types with type parameters mapped to their restrictive form, i.e. a form of the type parameter
12947+
// that has no constraint. This ensures that, for example, the type
12948+
// type Foo<T extends { x: any }> = T extends { x: string } ? string : number
12949+
// doesn't immediately resolve to 'string' instead of being deferred.
12950+
if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) {
12951+
result = instantiateTypeWithoutDepthIncrease(root.trueType, combinedMapper || mapper);
12952+
break;
12953+
}
12954+
}
12955+
// Return a deferred type for a check that is neither definitely true nor definitely false
12956+
const erasedCheckType = getActualTypeVariable(checkType);
12957+
result = <ConditionalType>createType(TypeFlags.Conditional);
12958+
result.root = root;
12959+
result.checkType = erasedCheckType;
12960+
result.extendsType = extendsType;
12961+
result.mapper = mapper;
12962+
result.combinedMapper = combinedMapper;
12963+
result.aliasSymbol = root.aliasSymbol;
12964+
result.aliasTypeArguments = instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217
12965+
break;
1289612966
}
12897-
const checkTypeInstantiable = isGenericObjectType(checkType) || isGenericIndexType(checkType);
12898-
let combinedMapper: TypeMapper | undefined;
12899-
if (root.inferTypeParameters) {
12900-
const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None);
12901-
// We skip inference of the possible `infer` types unles the `extendsType` _is_ an infer type
12902-
// if it was, it's trivial to say that extendsType = checkType, however such a pattern is used to
12903-
// "reset" the type being build up during constraint calculation and avoid making an apparently "infinite" constraint
12904-
// so in those cases we refain from performing inference and retain the uninfered type parameter
12905-
if (!checkTypeInstantiable || !some(root.inferTypeParameters, t => t === extendsType)) {
12906-
// We don't want inferences from constraints as they may cause us to eagerly resolve the
12907-
// conditional type instead of deferring resolution. Also, we always want strict function
12908-
// types rules (i.e. proper contravariance) for inferences.
12909-
inferTypes(context.inferences, checkType, extendsType, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
12910-
}
12911-
combinedMapper = mergeTypeMappers(mapper, context.mapper);
12912-
}
12913-
// Instantiate the extends type including inferences for 'infer T' type parameters
12914-
const inferredExtendsType = combinedMapper ? instantiateType(root.extendsType, combinedMapper) : extendsType;
12915-
// We attempt to resolve the conditional type only when the check and extends types are non-generic
12916-
if (!checkTypeInstantiable && !isGenericObjectType(inferredExtendsType) && !isGenericIndexType(inferredExtendsType)) {
12917-
if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown) {
12918-
return instantiateType(root.trueType, combinedMapper || mapper);
12919-
}
12920-
// Return union of trueType and falseType for 'any' since it matches anything
12921-
if (checkType.flags & TypeFlags.Any) {
12922-
return getUnionType([instantiateType(root.trueType, combinedMapper || mapper), instantiateType(root.falseType, mapper)]);
12923-
}
12924-
// Return falseType for a definitely false extends check. We check an instantiations of the two
12925-
// types with type parameters mapped to the wildcard type, the most permissive instantiations
12926-
// possible (the wildcard type is assignable to and from all types). If those are not related,
12927-
// then no instantiations will be and we can just return the false branch type.
12928-
if (!isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType))) {
12929-
return instantiateType(root.falseType, mapper);
12930-
}
12931-
// Return trueType for a definitely true extends check. We check instantiations of the two
12932-
// types with type parameters mapped to their restrictive form, i.e. a form of the type parameter
12933-
// that has no constraint. This ensures that, for example, the type
12934-
// type Foo<T extends { x: any }> = T extends { x: string } ? string : number
12935-
// doesn't immediately resolve to 'string' instead of being deferred.
12936-
if (isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) {
12937-
return instantiateType(root.trueType, combinedMapper || mapper);
12938-
}
12939-
}
12940-
// Return a deferred type for a check that is neither definitely true nor definitely false
12941-
const erasedCheckType = getActualTypeVariable(checkType);
12942-
const result = <ConditionalType>createType(TypeFlags.Conditional);
12943-
result.root = root;
12944-
result.checkType = erasedCheckType;
12945-
result.extendsType = extendsType;
12946-
result.mapper = mapper;
12947-
result.combinedMapper = combinedMapper;
12948-
result.aliasSymbol = root.aliasSymbol;
12949-
result.aliasTypeArguments = instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217
12950-
return result;
12967+
return extraTypes ? getUnionType(append(extraTypes, result)) : result;
1295112968
}
1295212969

1295312970
function getTrueTypeFromConditionalType(type: ConditionalType) {
@@ -13929,6 +13946,17 @@ namespace ts {
1392913946
return result;
1393013947
}
1393113948

13949+
/**
13950+
* This can be used to avoid the penalty on instantiation depth for types which result from immediate
13951+
* simplification. It essentially removes the depth increase done in `instantiateType`.
13952+
*/
13953+
function instantiateTypeWithoutDepthIncrease(type: Type, mapper: TypeMapper | undefined) {
13954+
instantiationDepth--;
13955+
const result = instantiateType(type, mapper);
13956+
instantiationDepth++;
13957+
return result;
13958+
}
13959+
1393213960
function instantiateTypeWorker(type: Type, mapper: TypeMapper): Type {
1393313961
const flags = type.flags;
1393413962
if (flags & TypeFlags.TypeParameter) {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//// [deeplyNestedConditionalTypes.ts]
2+
type Foo<T> =
3+
T extends 0 ? '0' :
4+
T extends 1 ? '1' :
5+
T extends 2 ? '2' :
6+
T extends 3 ? '3' :
7+
T extends 4 ? '4' :
8+
T extends 5 ? '5' :
9+
T extends 6 ? '6' :
10+
T extends 7 ? '7' :
11+
T extends 8 ? '8' :
12+
T extends 9 ? '9' :
13+
T extends 10 ? '10' :
14+
T extends 11 ? '11' :
15+
T extends 12 ? '12' :
16+
T extends 13 ? '13' :
17+
T extends 14 ? '14' :
18+
T extends 15 ? '15' :
19+
T extends 16 ? '16' :
20+
T extends 17 ? '17' :
21+
T extends 18 ? '18' :
22+
T extends 19 ? '19' :
23+
T extends 20 ? '20' :
24+
T extends 21 ? '21' :
25+
T extends 22 ? '22' :
26+
T extends 23 ? '23' :
27+
T extends 24 ? '24' :
28+
T extends 25 ? '25' :
29+
T extends 26 ? '26' :
30+
T extends 27 ? '27' :
31+
T extends 28 ? '28' :
32+
T extends 29 ? '29' :
33+
T extends 30 ? '30' :
34+
T extends 31 ? '31' :
35+
T extends 32 ? '32' :
36+
T extends 33 ? '33' :
37+
T extends 34 ? '34' :
38+
T extends 35 ? '35' :
39+
T extends 36 ? '36' :
40+
T extends 37 ? '37' :
41+
T extends 38 ? '38' :
42+
T extends 39 ? '39' :
43+
T extends 40 ? '40' :
44+
T extends 41 ? '41' :
45+
T extends 42 ? '42' :
46+
T extends 43 ? '43' :
47+
T extends 44 ? '44' :
48+
T extends 45 ? '45' :
49+
T extends 46 ? '46' :
50+
T extends 47 ? '47' :
51+
T extends 48 ? '48' :
52+
T extends 49 ? '49' :
53+
T extends 50 ? '50' :
54+
T extends 51 ? '51' :
55+
T extends 52 ? '52' :
56+
T extends 53 ? '53' :
57+
T extends 54 ? '54' :
58+
T extends 55 ? '55' :
59+
T extends 56 ? '56' :
60+
T extends 57 ? '57' :
61+
T extends 58 ? '58' :
62+
T extends 59 ? '59' :
63+
T extends 60 ? '60' :
64+
T extends 61 ? '61' :
65+
T extends 62 ? '62' :
66+
T extends 63 ? '63' :
67+
T extends 64 ? '64' :
68+
T extends 65 ? '65' :
69+
T extends 66 ? '66' :
70+
T extends 67 ? '67' :
71+
T extends 68 ? '68' :
72+
T extends 69 ? '69' :
73+
T extends 70 ? '70' :
74+
T extends 71 ? '71' :
75+
T extends 72 ? '72' :
76+
T extends 73 ? '73' :
77+
T extends 74 ? '74' :
78+
T extends 75 ? '75' :
79+
T extends 76 ? '76' :
80+
T extends 77 ? '77' :
81+
T extends 78 ? '78' :
82+
T extends 79 ? '79' :
83+
T extends 80 ? '80' :
84+
T extends 81 ? '81' :
85+
T extends 82 ? '82' :
86+
T extends 83 ? '83' :
87+
T extends 84 ? '84' :
88+
T extends 85 ? '85' :
89+
T extends 86 ? '86' :
90+
T extends 87 ? '87' :
91+
T extends 88 ? '88' :
92+
T extends 89 ? '89' :
93+
T extends 90 ? '90' :
94+
T extends 91 ? '91' :
95+
T extends 92 ? '92' :
96+
T extends 93 ? '93' :
97+
T extends 94 ? '94' :
98+
T extends 95 ? '95' :
99+
T extends 96 ? '96' :
100+
T extends 97 ? '97' :
101+
T extends 98 ? '98' :
102+
T extends 99 ? '99' :
103+
never;
104+
105+
type T0 = Foo<99>;
106+
type T1 = Foo<any>;
107+
108+
109+
//// [deeplyNestedConditionalTypes.js]
110+
"use strict";
111+
112+
113+
//// [deeplyNestedConditionalTypes.d.ts]
114+
declare type Foo<T> = T extends 0 ? '0' : T extends 1 ? '1' : T extends 2 ? '2' : T extends 3 ? '3' : T extends 4 ? '4' : T extends 5 ? '5' : T extends 6 ? '6' : T extends 7 ? '7' : T extends 8 ? '8' : T extends 9 ? '9' : T extends 10 ? '10' : T extends 11 ? '11' : T extends 12 ? '12' : T extends 13 ? '13' : T extends 14 ? '14' : T extends 15 ? '15' : T extends 16 ? '16' : T extends 17 ? '17' : T extends 18 ? '18' : T extends 19 ? '19' : T extends 20 ? '20' : T extends 21 ? '21' : T extends 22 ? '22' : T extends 23 ? '23' : T extends 24 ? '24' : T extends 25 ? '25' : T extends 26 ? '26' : T extends 27 ? '27' : T extends 28 ? '28' : T extends 29 ? '29' : T extends 30 ? '30' : T extends 31 ? '31' : T extends 32 ? '32' : T extends 33 ? '33' : T extends 34 ? '34' : T extends 35 ? '35' : T extends 36 ? '36' : T extends 37 ? '37' : T extends 38 ? '38' : T extends 39 ? '39' : T extends 40 ? '40' : T extends 41 ? '41' : T extends 42 ? '42' : T extends 43 ? '43' : T extends 44 ? '44' : T extends 45 ? '45' : T extends 46 ? '46' : T extends 47 ? '47' : T extends 48 ? '48' : T extends 49 ? '49' : T extends 50 ? '50' : T extends 51 ? '51' : T extends 52 ? '52' : T extends 53 ? '53' : T extends 54 ? '54' : T extends 55 ? '55' : T extends 56 ? '56' : T extends 57 ? '57' : T extends 58 ? '58' : T extends 59 ? '59' : T extends 60 ? '60' : T extends 61 ? '61' : T extends 62 ? '62' : T extends 63 ? '63' : T extends 64 ? '64' : T extends 65 ? '65' : T extends 66 ? '66' : T extends 67 ? '67' : T extends 68 ? '68' : T extends 69 ? '69' : T extends 70 ? '70' : T extends 71 ? '71' : T extends 72 ? '72' : T extends 73 ? '73' : T extends 74 ? '74' : T extends 75 ? '75' : T extends 76 ? '76' : T extends 77 ? '77' : T extends 78 ? '78' : T extends 79 ? '79' : T extends 80 ? '80' : T extends 81 ? '81' : T extends 82 ? '82' : T extends 83 ? '83' : T extends 84 ? '84' : T extends 85 ? '85' : T extends 86 ? '86' : T extends 87 ? '87' : T extends 88 ? '88' : T extends 89 ? '89' : T extends 90 ? '90' : T extends 91 ? '91' : T extends 92 ? '92' : T extends 93 ? '93' : T extends 94 ? '94' : T extends 95 ? '95' : T extends 96 ? '96' : T extends 97 ? '97' : T extends 98 ? '98' : T extends 99 ? '99' : never;
115+
declare type T0 = Foo<99>;
116+
declare type T1 = Foo<any>;

0 commit comments

Comments
 (0)