Improve constraints of conditional types applied to constrained type variables (#56004)

This commit is contained in:
Anders Hejlsberg
2023-11-20 17:08:22 -08:00
committed by GitHub
parent 3e094edc97
commit 38ef79e0b0
13 changed files with 763 additions and 193 deletions

View File

@@ -13679,7 +13679,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const checkType = (type as ConditionalType).checkType;
const constraint = getLowerBoundOfKeyType(checkType);
if (constraint !== checkType) {
return getConditionalTypeInstantiation(type as ConditionalType, prependTypeMapping((type as ConditionalType).root.checkType, constraint, (type as ConditionalType).mapper));
return getConditionalTypeInstantiation(type as ConditionalType, prependTypeMapping((type as ConditionalType).root.checkType, constraint, (type as ConditionalType).mapper), /*forConstraint*/ false);
}
}
return type;
@@ -14128,7 +14128,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const simplified = getSimplifiedType(type.checkType, /*writing*/ false);
const constraint = simplified === type.checkType ? getConstraintOfType(simplified) : simplified;
if (constraint && constraint !== type.checkType) {
const instantiated = getConditionalTypeInstantiation(type, prependTypeMapping(type.root.checkType, constraint, type.mapper));
const instantiated = getConditionalTypeInstantiation(type, prependTypeMapping(type.root.checkType, constraint, type.mapper), /*forConstraint*/ true);
if (!(instantiated.flags & TypeFlags.Never)) {
type.resolvedConstraintOfDistributive = instantiated;
return instantiated;
@@ -18353,7 +18353,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return isGenericType(type) || checkTuples && isTupleType(type) && some(getElementTypes(type), isGenericType);
}
function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined, forConstraint: boolean, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
let result;
let extraTypes: Type[] | undefined;
let tailCount = 0;
@@ -18432,8 +18432,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// 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) {
// Return union of trueType and falseType for 'any' since it matches anything. Furthermore, for a
// distributive conditional type applied to the constraint of a type variable, include trueType if
// there are possible values of the check type that are also possible values of the extends type.
// We use a reverse assignability check as it is less expensive than the comparable relationship
// and avoids false positives of a non-empty intersection check.
if (checkType.flags & TypeFlags.Any || forConstraint && !(inferredExtendsType.flags & TypeFlags.Never) && someType(getPermissiveInstantiation(inferredExtendsType), t => isTypeAssignableTo(t, getPermissiveInstantiation(checkType)))) {
(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
@@ -18557,7 +18561,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
aliasSymbol,
aliasTypeArguments,
};
links.resolvedType = getConditionalType(root, /*mapper*/ undefined);
links.resolvedType = getConditionalType(root, /*mapper*/ undefined, /*forConstraint*/ false);
if (outerTypeParameters) {
root.instantiations = new Map<string, Type>();
root.instantiations.set(getTypeListId(outerTypeParameters), links.resolvedType);
@@ -19565,14 +19569,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return result;
}
function getConditionalTypeInstantiation(type: ConditionalType, mapper: TypeMapper, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
function getConditionalTypeInstantiation(type: ConditionalType, mapper: TypeMapper, forConstraint: boolean, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
const root = type.root;
if (root.outerTypeParameters) {
// We are instantiating a conditional type that has one or more type parameters in scope. Apply the
// mapper to the type parameters to produce the effective list of type arguments, and compute the
// instantiation cache key from the type IDs of the type arguments.
const typeArguments = map(root.outerTypeParameters, t => getMappedType(t, mapper));
const id = getTypeListId(typeArguments) + getAliasId(aliasSymbol, aliasTypeArguments);
const id = (forConstraint ? "C" : "") + getTypeListId(typeArguments) + getAliasId(aliasSymbol, aliasTypeArguments);
let result = root.instantiations!.get(id);
if (!result) {
const newMapper = createTypeMapper(root.outerTypeParameters, typeArguments);
@@ -19582,8 +19586,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// distributive conditional type T extends U ? X : Y is instantiated with A | B for T, the
// result is (A extends U ? X : Y) | (B extends U ? X : Y).
result = distributionType && checkType !== distributionType && distributionType.flags & (TypeFlags.Union | TypeFlags.Never) ?
mapTypeWithAlias(getReducedType(distributionType), t => getConditionalType(root, prependTypeMapping(checkType, t, newMapper)), aliasSymbol, aliasTypeArguments) :
getConditionalType(root, newMapper, aliasSymbol, aliasTypeArguments);
mapTypeWithAlias(getReducedType(distributionType), t => getConditionalType(root, prependTypeMapping(checkType, t, newMapper), forConstraint), aliasSymbol, aliasTypeArguments) :
getConditionalType(root, newMapper, forConstraint, aliasSymbol, aliasTypeArguments);
root.instantiations!.set(id, result);
}
return result;
@@ -19665,7 +19669,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return getIndexedAccessType(instantiateType((type as IndexedAccessType).objectType, mapper), instantiateType((type as IndexedAccessType).indexType, mapper), (type as IndexedAccessType).accessFlags, /*accessNode*/ undefined, newAliasSymbol, newAliasTypeArguments);
}
if (flags & TypeFlags.Conditional) {
return getConditionalTypeInstantiation(type as ConditionalType, combineTypeMappers((type as ConditionalType).mapper, mapper), aliasSymbol, aliasTypeArguments);
return getConditionalTypeInstantiation(type as ConditionalType, combineTypeMappers((type as ConditionalType).mapper, mapper), /*forConstraint*/ false, aliasSymbol, aliasTypeArguments);
}
if (flags & TypeFlags.Substitution) {
const newBaseType = instantiateType((type as SubstitutionType).baseType, mapper);
@@ -22414,18 +22418,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
}
else {
// conditionals aren't related to one another via distributive constraint as it is much too inaccurate and allows way
// more assignments than are desirable (since it maps the source check type to its constraint, it loses information)
const distributiveConstraint = hasNonCircularBaseConstraint(source) ? getConstraintOfDistributiveConditionalType(source as ConditionalType) : undefined;
if (distributiveConstraint) {
if (result = isRelatedTo(distributiveConstraint, target, RecursionFlags.Source, reportErrors)) {
return result;
}
}
}
// conditionals _can_ be related to one another via normal constraint, as, eg, `A extends B ? O : never` should be assignable to `O`
// conditionals can be related to one another via normal constraint, as, eg, `A extends B ? O : never` should be assignable to `O`
// when `O` is a conditional (`never` is trivially assignable to `O`, as is `O`!).
const defaultConstraint = getDefaultConstraintOfConditionalType(source as ConditionalType);
if (defaultConstraint) {
@@ -22433,6 +22426,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return result;
}
}
// conditionals aren't related to one another via distributive constraint as it is much too inaccurate and allows way
// more assignments than are desirable (since it maps the source check type to its constraint, it loses information).
const distributiveConstraint = !(targetFlags & TypeFlags.Conditional) && hasNonCircularBaseConstraint(source) ? getConstraintOfDistributiveConditionalType(source as ConditionalType) : undefined;
if (distributiveConstraint) {
resetErrorInfo(saveErrorInfo);
if (result = isRelatedTo(distributiveConstraint, target, RecursionFlags.Source, reportErrors)) {
return result;
}
}
}
else {
// An empty object type is related to any mapped type that includes a '?' modifier.