Skip to content

Commit 38ef79e

Browse files
authored
Improve constraints of conditional types applied to constrained type variables (#56004)
1 parent 3e094ed commit 38ef79e

13 files changed

+763
-193
lines changed

src/compiler/checker.ts

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13679,7 +13679,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1367913679
const checkType = (type as ConditionalType).checkType;
1368013680
const constraint = getLowerBoundOfKeyType(checkType);
1368113681
if (constraint !== checkType) {
13682-
return getConditionalTypeInstantiation(type as ConditionalType, prependTypeMapping((type as ConditionalType).root.checkType, constraint, (type as ConditionalType).mapper));
13682+
return getConditionalTypeInstantiation(type as ConditionalType, prependTypeMapping((type as ConditionalType).root.checkType, constraint, (type as ConditionalType).mapper), /*forConstraint*/ false);
1368313683
}
1368413684
}
1368513685
return type;
@@ -14128,7 +14128,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1412814128
const simplified = getSimplifiedType(type.checkType, /*writing*/ false);
1412914129
const constraint = simplified === type.checkType ? getConstraintOfType(simplified) : simplified;
1413014130
if (constraint && constraint !== type.checkType) {
14131-
const instantiated = getConditionalTypeInstantiation(type, prependTypeMapping(type.root.checkType, constraint, type.mapper));
14131+
const instantiated = getConditionalTypeInstantiation(type, prependTypeMapping(type.root.checkType, constraint, type.mapper), /*forConstraint*/ true);
1413214132
if (!(instantiated.flags & TypeFlags.Never)) {
1413314133
type.resolvedConstraintOfDistributive = instantiated;
1413414134
return instantiated;
@@ -18353,7 +18353,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1835318353
return isGenericType(type) || checkTuples && isTupleType(type) && some(getElementTypes(type), isGenericType);
1835418354
}
1835518355

18356-
function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
18356+
function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined, forConstraint: boolean, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
1835718357
let result;
1835818358
let extraTypes: Type[] | undefined;
1835918359
let tailCount = 0;
@@ -18432,8 +18432,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1843218432
// possible (the wildcard type is assignable to and from all types). If those are not related,
1843318433
// then no instantiations will be and we can just return the false branch type.
1843418434
if (!(inferredExtendsType.flags & TypeFlags.AnyOrUnknown) && (checkType.flags & TypeFlags.Any || !isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType)))) {
18435-
// Return union of trueType and falseType for 'any' since it matches anything
18436-
if (checkType.flags & TypeFlags.Any) {
18435+
// Return union of trueType and falseType for 'any' since it matches anything. Furthermore, for a
18436+
// distributive conditional type applied to the constraint of a type variable, include trueType if
18437+
// there are possible values of the check type that are also possible values of the extends type.
18438+
// We use a reverse assignability check as it is less expensive than the comparable relationship
18439+
// and avoids false positives of a non-empty intersection check.
18440+
if (checkType.flags & TypeFlags.Any || forConstraint && !(inferredExtendsType.flags & TypeFlags.Never) && someType(getPermissiveInstantiation(inferredExtendsType), t => isTypeAssignableTo(t, getPermissiveInstantiation(checkType)))) {
1843718441
(extraTypes || (extraTypes = [])).push(instantiateType(getTypeFromTypeNode(root.node.trueType), combinedMapper || mapper));
1843818442
}
1843918443
// If falseType is an immediately nested conditional type that isn't distributive or has an
@@ -18557,7 +18561,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1855718561
aliasSymbol,
1855818562
aliasTypeArguments,
1855918563
};
18560-
links.resolvedType = getConditionalType(root, /*mapper*/ undefined);
18564+
links.resolvedType = getConditionalType(root, /*mapper*/ undefined, /*forConstraint*/ false);
1856118565
if (outerTypeParameters) {
1856218566
root.instantiations = new Map<string, Type>();
1856318567
root.instantiations.set(getTypeListId(outerTypeParameters), links.resolvedType);
@@ -19565,14 +19569,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1956519569
return result;
1956619570
}
1956719571

19568-
function getConditionalTypeInstantiation(type: ConditionalType, mapper: TypeMapper, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
19572+
function getConditionalTypeInstantiation(type: ConditionalType, mapper: TypeMapper, forConstraint: boolean, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
1956919573
const root = type.root;
1957019574
if (root.outerTypeParameters) {
1957119575
// We are instantiating a conditional type that has one or more type parameters in scope. Apply the
1957219576
// mapper to the type parameters to produce the effective list of type arguments, and compute the
1957319577
// instantiation cache key from the type IDs of the type arguments.
1957419578
const typeArguments = map(root.outerTypeParameters, t => getMappedType(t, mapper));
19575-
const id = getTypeListId(typeArguments) + getAliasId(aliasSymbol, aliasTypeArguments);
19579+
const id = (forConstraint ? "C" : "") + getTypeListId(typeArguments) + getAliasId(aliasSymbol, aliasTypeArguments);
1957619580
let result = root.instantiations!.get(id);
1957719581
if (!result) {
1957819582
const newMapper = createTypeMapper(root.outerTypeParameters, typeArguments);
@@ -19582,8 +19586,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1958219586
// distributive conditional type T extends U ? X : Y is instantiated with A | B for T, the
1958319587
// result is (A extends U ? X : Y) | (B extends U ? X : Y).
1958419588
result = distributionType && checkType !== distributionType && distributionType.flags & (TypeFlags.Union | TypeFlags.Never) ?
19585-
mapTypeWithAlias(getReducedType(distributionType), t => getConditionalType(root, prependTypeMapping(checkType, t, newMapper)), aliasSymbol, aliasTypeArguments) :
19586-
getConditionalType(root, newMapper, aliasSymbol, aliasTypeArguments);
19589+
mapTypeWithAlias(getReducedType(distributionType), t => getConditionalType(root, prependTypeMapping(checkType, t, newMapper), forConstraint), aliasSymbol, aliasTypeArguments) :
19590+
getConditionalType(root, newMapper, forConstraint, aliasSymbol, aliasTypeArguments);
1958719591
root.instantiations!.set(id, result);
1958819592
}
1958919593
return result;
@@ -19665,7 +19669,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1966519669
return getIndexedAccessType(instantiateType((type as IndexedAccessType).objectType, mapper), instantiateType((type as IndexedAccessType).indexType, mapper), (type as IndexedAccessType).accessFlags, /*accessNode*/ undefined, newAliasSymbol, newAliasTypeArguments);
1966619670
}
1966719671
if (flags & TypeFlags.Conditional) {
19668-
return getConditionalTypeInstantiation(type as ConditionalType, combineTypeMappers((type as ConditionalType).mapper, mapper), aliasSymbol, aliasTypeArguments);
19672+
return getConditionalTypeInstantiation(type as ConditionalType, combineTypeMappers((type as ConditionalType).mapper, mapper), /*forConstraint*/ false, aliasSymbol, aliasTypeArguments);
1966919673
}
1967019674
if (flags & TypeFlags.Substitution) {
1967119675
const newBaseType = instantiateType((type as SubstitutionType).baseType, mapper);
@@ -22414,25 +22418,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2241422418
}
2241522419
}
2241622420
}
22417-
else {
22418-
// conditionals aren't related to one another via distributive constraint as it is much too inaccurate and allows way
22419-
// more assignments than are desirable (since it maps the source check type to its constraint, it loses information)
22420-
const distributiveConstraint = hasNonCircularBaseConstraint(source) ? getConstraintOfDistributiveConditionalType(source as ConditionalType) : undefined;
22421-
if (distributiveConstraint) {
22422-
if (result = isRelatedTo(distributiveConstraint, target, RecursionFlags.Source, reportErrors)) {
22423-
return result;
22424-
}
22425-
}
22426-
}
22427-
22428-
// conditionals _can_ be related to one another via normal constraint, as, eg, `A extends B ? O : never` should be assignable to `O`
22421+
// conditionals can be related to one another via normal constraint, as, eg, `A extends B ? O : never` should be assignable to `O`
2242922422
// when `O` is a conditional (`never` is trivially assignable to `O`, as is `O`!).
2243022423
const defaultConstraint = getDefaultConstraintOfConditionalType(source as ConditionalType);
2243122424
if (defaultConstraint) {
2243222425
if (result = isRelatedTo(defaultConstraint, target, RecursionFlags.Source, reportErrors)) {
2243322426
return result;
2243422427
}
2243522428
}
22429+
// conditionals aren't related to one another via distributive constraint as it is much too inaccurate and allows way
22430+
// more assignments than are desirable (since it maps the source check type to its constraint, it loses information).
22431+
const distributiveConstraint = !(targetFlags & TypeFlags.Conditional) && hasNonCircularBaseConstraint(source) ? getConstraintOfDistributiveConditionalType(source as ConditionalType) : undefined;
22432+
if (distributiveConstraint) {
22433+
resetErrorInfo(saveErrorInfo);
22434+
if (result = isRelatedTo(distributiveConstraint, target, RecursionFlags.Source, reportErrors)) {
22435+
return result;
22436+
}
22437+
}
2243622438
}
2243722439
else {
2243822440
// An empty object type is related to any mapped type that includes a '?' modifier.

0 commit comments

Comments
 (0)