Skip to content

Commit 16c1d9d

Browse files
committed
Reduce intersections of constrained type variables and primitive types
1 parent b9ae791 commit 16c1d9d

File tree

2 files changed

+85
-3
lines changed

2 files changed

+85
-3
lines changed

src/compiler/checker.ts

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16794,6 +16794,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1679416794
if (!(flags & TypeFlags.Never)) {
1679516795
includes |= flags & TypeFlags.IncludesMask;
1679616796
if (flags & TypeFlags.Instantiable) includes |= TypeFlags.IncludesInstantiable;
16797+
if (flags & TypeFlags.Intersection && getObjectFlags(type) & ObjectFlags.IsConstrainedTypeVariable) includes |= TypeFlags.IncludesConstrainedTypeVariable;
1679716798
if (type === wildcardType) includes |= TypeFlags.IncludesWildcard;
1679816799
if (!strictNullChecks && flags & TypeFlags.Nullable) {
1679916800
if (!(getObjectFlags(type) & ObjectFlags.ContainsWideningType)) includes |= TypeFlags.IncludesNonWideningType;
@@ -16938,6 +16939,49 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1693816939
}
1693916940
}
1694016941

16942+
function removeConstrainedTypeVariables(types: Type[]) {
16943+
const typeVariables: TypeVariable[] = [];
16944+
// First collect a list of the type variables occurring in constraining intersections.
16945+
for (const type of types) {
16946+
if (getObjectFlags(type) & ObjectFlags.IsConstrainedTypeVariable) {
16947+
const index = (type as IntersectionType).types[0].flags & TypeFlags.TypeVariable ? 0 : 1;
16948+
pushIfUnique(typeVariables, (type as IntersectionType).types[index]);
16949+
}
16950+
}
16951+
// For each type variable, check if the constraining intersections for that type variable fully
16952+
// cover the constraint of the type variable; if so, remove the constraining intersections and
16953+
// substitute the type variable.
16954+
for (const typeVariable of typeVariables) {
16955+
const primitives: Type[] = [];
16956+
// First collect the primitive types from the constraining intersections.
16957+
for (const type of types) {
16958+
if (getObjectFlags(type) & ObjectFlags.IsConstrainedTypeVariable) {
16959+
const index = (type as IntersectionType).types[0].flags & TypeFlags.TypeVariable ? 0 : 1;
16960+
if ((type as IntersectionType).types[index] === typeVariable) {
16961+
insertType(primitives, (type as IntersectionType).types[1 - index]);
16962+
}
16963+
}
16964+
}
16965+
// If every constituent in the type variable's constraint is covered by an intersection of the type
16966+
// variable and that constituent, remove those intersections and substitute the type variable.
16967+
const constraint = getBaseConstraintOfType(typeVariable)!;
16968+
if (everyType(constraint, t => containsType(primitives, t))) {
16969+
let i = types.length;
16970+
while (i > 0) {
16971+
i--;
16972+
const type = types[i];
16973+
if (getObjectFlags(type) & ObjectFlags.IsConstrainedTypeVariable) {
16974+
const index = (type as IntersectionType).types[0].flags & TypeFlags.TypeVariable ? 0 : 1;
16975+
if ((type as IntersectionType).types[index] === typeVariable && containsType(primitives, (type as IntersectionType).types[1 - index])) {
16976+
orderedRemoveItemAt(types, i);
16977+
}
16978+
}
16979+
}
16980+
insertType(types, typeVariable);
16981+
}
16982+
}
16983+
}
16984+
1694116985
function isNamedUnionType(type: Type) {
1694216986
return !!(type.flags & TypeFlags.Union && (type.aliasSymbol || (type as UnionType).origin));
1694316987
}
@@ -17012,6 +17056,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1701217056
if (includes & TypeFlags.StringLiteral && includes & TypeFlags.TemplateLiteral) {
1701317057
removeStringLiteralsMatchedByTemplateLiterals(typeSet);
1701417058
}
17059+
if (includes & TypeFlags.IncludesConstrainedTypeVariable) {
17060+
removeConstrainedTypeVariables(typeSet);
17061+
}
1701517062
if (unionReduction === UnionReduction.Subtype) {
1701617063
typeSet = removeSubtypes(typeSet, !!(includes & TypeFlags.Object));
1701717064
if (!typeSet) {
@@ -17276,9 +17323,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1727617323
return true;
1727717324
}
1727817325

17279-
function createIntersectionType(types: Type[], aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]) {
17326+
function createIntersectionType(types: Type[], objectFlags: ObjectFlags, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]) {
1728017327
const result = createType(TypeFlags.Intersection) as IntersectionType;
17281-
result.objectFlags = getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable);
17328+
result.objectFlags = objectFlags | getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable);
1728217329
result.types = types;
1728317330
result.aliasSymbol = aliasSymbol;
1728417331
result.aliasTypeArguments = aliasTypeArguments;
@@ -17299,6 +17346,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1729917346
const typeMembershipMap = new Map<string, Type>();
1730017347
const includes = addTypesToIntersection(typeMembershipMap, 0 as TypeFlags, types);
1730117348
const typeSet: Type[] = arrayFrom(typeMembershipMap.values());
17349+
let objectFlags = ObjectFlags.None;
1730217350
// An intersection type is considered empty if it contains
1730317351
// the type never, or
1730417352
// more than one unit type or,
@@ -17350,6 +17398,36 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1735017398
if (typeSet.length === 1) {
1735117399
return typeSet[0];
1735217400
}
17401+
if (typeSet.length === 2) {
17402+
const typeVarIndex = typeSet[0].flags & TypeFlags.TypeVariable ? 0 : 1;
17403+
const typeVariable = typeSet[typeVarIndex];
17404+
const primitiveType = typeSet[1 - typeVarIndex];
17405+
if (typeVariable.flags & TypeFlags.TypeVariable && (primitiveType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive) || includes & TypeFlags.IncludesEmptyObject)) {
17406+
// We have an intersection T & P or P & T, where T is a type variable and P is a primitive type, the object type, or {}.
17407+
const constraint = getBaseConstraintOfType(typeVariable);
17408+
// Check that T's constraint is similarly composed of primitive types, the object type, or {}.
17409+
if (constraint && everyType(constraint, t => !!(t.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) || isEmptyAnonymousObjectType(t))) {
17410+
// If T's constraint is a subtype of P, simply return T. For example, given `T extends "a" | "b"`,
17411+
// the intersection `T & string` reduces to just T.
17412+
if (isTypeStrictSubtypeOf(constraint, primitiveType)) {
17413+
return typeVariable;
17414+
}
17415+
if (!(constraint.flags & TypeFlags.Union && someType(constraint, c => isTypeStrictSubtypeOf(c, primitiveType)))) {
17416+
// No constituent of T's constraint is a subtype of P. If P is also not a subtype of T's constraint,
17417+
// then the constraint and P are unrelated, and the intersection reduces to never. For example, given
17418+
// `T extends "a" | "b"`, the intersection `T & number` reduces to never.
17419+
if (!isTypeStrictSubtypeOf(primitiveType, constraint)) {
17420+
return neverType;
17421+
}
17422+
}
17423+
// Some constituent of T's constraint is a subtype of P, or P is a subtype of T's constraint. Thus,
17424+
// the intersection further constrains the type variable. For example, given `T extends string | number`,
17425+
// the intersection `T & "a"` is marked as a constrained type variable. Likewise, given `T extends "a" | 1`,
17426+
// the intersection `T & number` is marked as a constrained type variable.
17427+
objectFlags = ObjectFlags.IsConstrainedTypeVariable;
17428+
}
17429+
}
17430+
}
1735317431
const id = getTypeListId(typeSet) + getAliasId(aliasSymbol, aliasTypeArguments);
1735417432
let result = intersectionTypes.get(id);
1735517433
if (!result) {
@@ -17385,7 +17463,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1738517463
}
1738617464
}
1738717465
else {
17388-
result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
17466+
result = createIntersectionType(typeSet, objectFlags, aliasSymbol, aliasTypeArguments);
1738917467
}
1739017468
intersectionTypes.set(id, result);
1739117469
}

src/compiler/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6153,6 +6153,8 @@ export const enum TypeFlags {
61536153
/** @internal */
61546154
IncludesInstantiable = Substitution,
61556155
/** @internal */
6156+
IncludesConstrainedTypeVariable = StringMapping,
6157+
/** @internal */
61566158
NotPrimitiveUnion = Any | Unknown | Void | Never | Object | Intersection | IncludesInstantiable,
61576159
}
61586160

@@ -6313,6 +6315,8 @@ export const enum ObjectFlags {
63136315
IsNeverIntersectionComputed = 1 << 24, // IsNeverLike flag has been computed
63146316
/** @internal */
63156317
IsNeverIntersection = 1 << 25, // Intersection reduces to never
6318+
/** @internal */
6319+
IsConstrainedTypeVariable = 1 << 26, // T & C, where T's constraint and C are primitives, object, or {}
63166320
}
63176321

63186322
/** @internal */

0 commit comments

Comments
 (0)