Skip to content

Flatten immediately nested conditional types in the false position #36583

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
merged 7 commits into from
Mar 16, 2020
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
144 changes: 86 additions & 58 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12887,65 +12887,82 @@ namespace ts {
}

function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined): Type {
const checkType = instantiateType(root.checkType, mapper);
const extendsType = instantiateType(root.extendsType, mapper);
if (checkType === wildcardType || extendsType === wildcardType) {
return wildcardType;
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 checkTypeInstantiable = isGenericObjectType(checkType) || isGenericIndexType(checkType);
const extendsType = instantiateType(root.extendsType, mapper);
if (checkType === wildcardType || extendsType === wildcardType) {
return wildcardType;
}
let combinedMapper: TypeMapper | undefined;
if (root.inferTypeParameters) {
const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None);
// We skip inference of the possible `infer` types unles the `extendsType` _is_ an infer type
// if it was, it's trivial to say that extendsType = checkType, however such a pattern is used to
// "reset" the type being build up during constraint calculation and avoid making an apparently "infinite" constraint
// so in those cases we refain from performing inference and retain the uninfered type parameter
if (!checkTypeInstantiable || !some(root.inferTypeParameters, t => t === extendsType)) {
// We don't want inferences from constraints as they may cause us to eagerly resolve the
// conditional type instead of deferring resolution. Also, we always want strict function
// types rules (i.e. proper contravariance) for inferences.
inferTypes(context.inferences, checkType, extendsType, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
}
combinedMapper = mergeTypeMappers(mapper, context.mapper);
}
// Instantiate the extends type including inferences for 'infer T' type parameters
const inferredExtendsType = combinedMapper ? instantiateType(root.extendsType, combinedMapper) : extendsType;
// We attempt to resolve the conditional type only when the check and extends types are non-generic
if (!checkTypeInstantiable && !isGenericObjectType(inferredExtendsType) && !isGenericIndexType(inferredExtendsType)) {
// Return falseType for a definitely false extends check. We check an instantiations of the two
// 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)))) {
// Return union of trueType and falseType for 'any' since it matches anything
if (checkType.flags & TypeFlags.Any) {
(extraTypes || (extraTypes = [])).push(instantiateTypeWithoutDepthIncrease(root.trueType, combinedMapper || mapper));
}
// If falseType is an immediately nested conditional type that isn't distributive or has an
// identical checkType, switch to that type and loop.
const falseType = root.falseType;
if (falseType.flags & TypeFlags.Conditional) {
const newRoot = (<ConditionalType>falseType).root;
if (newRoot.node.parent === root.node && (!newRoot.isDistributive || newRoot.checkType === root.checkType)) {
root = newRoot;
continue;
}
}
result = instantiateTypeWithoutDepthIncrease(falseType, mapper);
break;
}
// Return trueType for a definitely true extends check. We check instantiations of the two
// types with type parameters mapped to their restrictive form, i.e. a form of the type parameter
// that has no constraint. This ensures that, for example, the type
// type Foo<T extends { x: any }> = T extends { x: string } ? string : number
// doesn't immediately resolve to 'string' instead of being deferred.
if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) {
result = instantiateTypeWithoutDepthIncrease(root.trueType, combinedMapper || mapper);
break;
}
}
// Return a deferred type for a check that is neither definitely true nor definitely false
const erasedCheckType = getActualTypeVariable(checkType);
result = <ConditionalType>createType(TypeFlags.Conditional);
result.root = root;
result.checkType = erasedCheckType;
result.extendsType = extendsType;
result.mapper = mapper;
result.combinedMapper = combinedMapper;
result.aliasSymbol = root.aliasSymbol;
result.aliasTypeArguments = instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217
break;
}
const checkTypeInstantiable = isGenericObjectType(checkType) || isGenericIndexType(checkType);
let combinedMapper: TypeMapper | undefined;
if (root.inferTypeParameters) {
const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None);
// We skip inference of the possible `infer` types unles the `extendsType` _is_ an infer type
// if it was, it's trivial to say that extendsType = checkType, however such a pattern is used to
// "reset" the type being build up during constraint calculation and avoid making an apparently "infinite" constraint
// so in those cases we refain from performing inference and retain the uninfered type parameter
if (!checkTypeInstantiable || !some(root.inferTypeParameters, t => t === extendsType)) {
// We don't want inferences from constraints as they may cause us to eagerly resolve the
// conditional type instead of deferring resolution. Also, we always want strict function
// types rules (i.e. proper contravariance) for inferences.
inferTypes(context.inferences, checkType, extendsType, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
}
combinedMapper = mergeTypeMappers(mapper, context.mapper);
}
// Instantiate the extends type including inferences for 'infer T' type parameters
const inferredExtendsType = combinedMapper ? instantiateType(root.extendsType, combinedMapper) : extendsType;
// We attempt to resolve the conditional type only when the check and extends types are non-generic
if (!checkTypeInstantiable && !isGenericObjectType(inferredExtendsType) && !isGenericIndexType(inferredExtendsType)) {
if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown) {
return instantiateType(root.trueType, combinedMapper || mapper);
}
// Return union of trueType and falseType for 'any' since it matches anything
if (checkType.flags & TypeFlags.Any) {
return getUnionType([instantiateType(root.trueType, combinedMapper || mapper), instantiateType(root.falseType, mapper)]);
}
// Return falseType for a definitely false extends check. We check an instantiations of the two
// 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 (!isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType))) {
return instantiateType(root.falseType, mapper);
}
// Return trueType for a definitely true extends check. We check instantiations of the two
// types with type parameters mapped to their restrictive form, i.e. a form of the type parameter
// that has no constraint. This ensures that, for example, the type
// type Foo<T extends { x: any }> = T extends { x: string } ? string : number
// doesn't immediately resolve to 'string' instead of being deferred.
if (isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) {
return instantiateType(root.trueType, combinedMapper || mapper);
}
}
// Return a deferred type for a check that is neither definitely true nor definitely false
const erasedCheckType = getActualTypeVariable(checkType);
const result = <ConditionalType>createType(TypeFlags.Conditional);
result.root = root;
result.checkType = erasedCheckType;
result.extendsType = extendsType;
result.mapper = mapper;
result.combinedMapper = combinedMapper;
result.aliasSymbol = root.aliasSymbol;
result.aliasTypeArguments = instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217
return result;
return extraTypes ? getUnionType(append(extraTypes, result)) : result;
}

function getTrueTypeFromConditionalType(type: ConditionalType) {
Expand Down Expand Up @@ -13923,6 +13940,17 @@ namespace ts {
return result;
}

/**
* This can be used to avoid the penalty on instantiation depth for types which result from immediate
* simplification. It essentially removes the depth increase done in `instantiateType`.
*/
function instantiateTypeWithoutDepthIncrease(type: Type, mapper: TypeMapper | undefined) {
instantiationDepth--;
const result = instantiateType(type, mapper);
instantiationDepth++;
return result;
}

function instantiateTypeWorker(type: Type, mapper: TypeMapper): Type {
const flags = type.flags;
if (flags & TypeFlags.TypeParameter) {
Expand Down
116 changes: 116 additions & 0 deletions tests/baselines/reference/deeplyNestedConditionalTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//// [deeplyNestedConditionalTypes.ts]
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;

type T0 = Foo<99>;
type T1 = Foo<any>;


//// [deeplyNestedConditionalTypes.js]
"use strict";


//// [deeplyNestedConditionalTypes.d.ts]
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;
declare type T0 = Foo<99>;
declare type T1 = Foo<any>;
Loading