Skip to content
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

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

Merged
merged 9 commits into from
Nov 21, 2023
Merged
42 changes: 20 additions & 22 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13571,7 +13571,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;
Expand Down Expand Up @@ -14020,7 +14020,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;
Expand Down Expand Up @@ -18243,7 +18243,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;
Expand Down Expand Up @@ -18323,7 +18323,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// 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) {
if (checkType.flags & TypeFlags.Any || forConstraint && isTypeAssignableTo(getPermissiveInstantiation(inferredExtendsType), getPermissiveInstantiation(checkType))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this is probably a bit better than the often-incorrect result we give now, so maybe we're just fine with that being that, isn't a check in this style kind of fundamentally flawed? For example, a type Cond<T extends {a: string} | {b: string}> = T extends {c: string} ? true : false. T's constraint isn't related in any way to the check type of {c: string}, yet the constraint should probably still be the union of both branches, as an instantiation of T might satisfy the check type, or might not, while here we're still skipping the true branch type just because {a: string} isn't assignable to {c: string}. And comparability wouldn't have been any better. As @andrewbranch said - the only case where we can definitively say only one branch is taken would seem to require checking for an empty intersection between the check-type-constraint and the extends type (and, as you mentioned, ideally tighter empty intersection logic so that has higher accuracy).

How different are the results it if we just replace this assignability check with a intersectTypes(getPermissiveInstantiation(inferredExtendsType), getPermissiveInstantiation(checkType)) !== neverType? Are the bad changes any worse?

(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
Expand Down Expand Up @@ -18447,7 +18447,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);
Expand Down Expand Up @@ -19454,14 +19454,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);
Expand All @@ -19471,8 +19471,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;
Expand Down Expand Up @@ -19554,7 +19554,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);
Expand Down Expand Up @@ -22303,25 +22303,23 @@ 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) {
if (result = isRelatedTo(defaultConstraint, target, RecursionFlags.Source, reportErrors)) {
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.
Expand Down
Loading
Loading