Skip to content

Disable constraint reduction in intersections created by constraint hoisting #58403

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 4 commits into from
May 2, 2024
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
31 changes: 16 additions & 15 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ import {
InterfaceType,
InterfaceTypeWithDeclaredMembers,
InternalSymbolName,
IntersectionFlags,
IntersectionType,
IntersectionTypeNode,
intrinsicTagNameToString,
Expand Down Expand Up @@ -14204,7 +14205,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
// The source types were normalized; ensure the result is normalized too.
return getNormalizedType(getIntersectionType(constraints), /*writing*/ false);
return getNormalizedType(getIntersectionType(constraints, IntersectionFlags.NoConstraintReduction), /*writing*/ false);
}
return undefined;
}
Expand Down Expand Up @@ -17467,7 +17468,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// a type alias of the form "type List<T> = T & { next: List<T> }" cannot be reduced during its declaration.
// Also, unlike union types, the order of the constituent types is preserved in order that overload resolution
// for intersections of types with signatures can be deterministic.
function getIntersectionType(types: readonly Type[], aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[], noSupertypeReduction?: boolean): Type {
function getIntersectionType(types: readonly Type[], flags = IntersectionFlags.None, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
Copy link
Member

Choose a reason for hiding this comment

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

Is this flag something that should be required, in case we forget to pass it? Happens all the time for the relation intersection state...

Copy link
Member Author

Choose a reason for hiding this comment

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

No, it's exceedingly rare to need any of these flags (only one place for each flag in entire code base), so I think it's preferable to leave it optional.

const typeMembershipMap = new Map<string, Type>();
const includes = addTypesToIntersection(typeMembershipMap, 0 as TypeFlags, types);
const typeSet: Type[] = arrayFrom(typeMembershipMap.values());
Expand Down Expand Up @@ -17512,7 +17513,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
includes & TypeFlags.Void && includes & TypeFlags.Undefined ||
includes & TypeFlags.IncludesEmptyObject && includes & TypeFlags.DefinitelyNonNullable
) {
if (!noSupertypeReduction) removeRedundantSupertypes(typeSet, includes);
if (!(flags & IntersectionFlags.NoSupertypeReduction)) removeRedundantSupertypes(typeSet, includes);
}
if (includes & TypeFlags.IncludesMissingType) {
typeSet[typeSet.indexOf(undefinedType)] = missingType;
Expand All @@ -17523,7 +17524,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (typeSet.length === 1) {
return typeSet[0];
}
if (typeSet.length === 2) {
if (typeSet.length === 2 && !(flags & IntersectionFlags.NoConstraintReduction)) {
const typeVarIndex = typeSet[0].flags & TypeFlags.TypeVariable ? 0 : 1;
const typeVariable = typeSet[typeVarIndex];
const primitiveType = typeSet[1 - typeVarIndex];
Expand Down Expand Up @@ -17556,32 +17557,32 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
}
const id = getTypeListId(typeSet) + getAliasId(aliasSymbol, aliasTypeArguments);
const id = getTypeListId(typeSet) + (flags & IntersectionFlags.NoConstraintReduction ? "*" : getAliasId(aliasSymbol, aliasTypeArguments));
let result = intersectionTypes.get(id);
if (!result) {
if (includes & TypeFlags.Union) {
if (intersectUnionsOfPrimitiveTypes(typeSet)) {
// When the intersection creates a reduced set (which might mean that *all* union types have
// disappeared), we restart the operation to get a new set of combined flags. Once we have
// reduced we'll never reduce again, so this occurs at most once.
result = getIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
result = getIntersectionType(typeSet, flags, aliasSymbol, aliasTypeArguments);
}
else if (every(typeSet, t => !!(t.flags & TypeFlags.Union && (t as UnionType).types[0].flags & TypeFlags.Undefined))) {
const containedUndefinedType = some(typeSet, containsMissingType) ? missingType : undefinedType;
removeFromEach(typeSet, TypeFlags.Undefined);
result = getUnionType([getIntersectionType(typeSet), containedUndefinedType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
result = getUnionType([getIntersectionType(typeSet, flags), containedUndefinedType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
}
else if (every(typeSet, t => !!(t.flags & TypeFlags.Union && ((t as UnionType).types[0].flags & TypeFlags.Null || (t as UnionType).types[1].flags & TypeFlags.Null)))) {
removeFromEach(typeSet, TypeFlags.Null);
result = getUnionType([getIntersectionType(typeSet), nullType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
result = getUnionType([getIntersectionType(typeSet, flags), nullType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
}
else if (typeSet.length >= 4) {
// When we have four or more constituents, some of which are unions, we employ a "divide and conquer" strategy
// where A & B & C & D is processed as (A & B) & (C & D). Since intersections of unions often produce far smaller
// unions of intersections than the full cartesian product (due to some intersections becoming `never`), this can
// dramatically reduce the overall work.
const middle = Math.floor(typeSet.length / 2);
result = getIntersectionType([getIntersectionType(typeSet.slice(0, middle)), getIntersectionType(typeSet.slice(middle))], aliasSymbol, aliasTypeArguments);
result = getIntersectionType([getIntersectionType(typeSet.slice(0, middle), flags), getIntersectionType(typeSet.slice(middle), flags)], flags, aliasSymbol, aliasTypeArguments);
}
else {
// We are attempting to construct a type of the form X & (A | B) & (C | D). Transform this into a type of
Expand All @@ -17590,7 +17591,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (!checkCrossProductUnion(typeSet)) {
return errorType;
}
const constituents = getCrossProductIntersections(typeSet);
const constituents = getCrossProductIntersections(typeSet, flags);
// We attach a denormalized origin type when at least one constituent of the cross-product union is an
// intersection (i.e. when the intersection didn't just reduce one or more unions to smaller unions) and
// the denormalized origin has fewer constituents than the union itself.
Expand Down Expand Up @@ -17620,7 +17621,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return true;
}

function getCrossProductIntersections(types: readonly Type[]) {
function getCrossProductIntersections(types: readonly Type[], flags: IntersectionFlags) {
const count = getCrossProductUnionSize(types);
const intersections: Type[] = [];
for (let i = 0; i < count; i++) {
Expand All @@ -17634,7 +17635,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
n = Math.floor(n / length);
}
}
const t = getIntersectionType(constituents);
const t = getIntersectionType(constituents, flags);
if (!(t.flags & TypeFlags.Never)) intersections.push(t);
}
return intersections;
Expand All @@ -17661,7 +17662,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const emptyIndex = types.length === 2 ? types.indexOf(emptyTypeLiteralType) : -1;
const t = emptyIndex >= 0 ? types[1 - emptyIndex] : unknownType;
const noSupertypeReduction = !!(t.flags & (TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt) || t.flags & TypeFlags.TemplateLiteral && isPatternLiteralType(t));
links.resolvedType = getIntersectionType(types, aliasSymbol, getTypeArgumentsForAliasSymbol(aliasSymbol), noSupertypeReduction);
links.resolvedType = getIntersectionType(types, noSupertypeReduction ? IntersectionFlags.NoSupertypeReduction : 0, aliasSymbol, getTypeArgumentsForAliasSymbol(aliasSymbol));
}
return links.resolvedType;
}
Expand Down Expand Up @@ -18541,7 +18542,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return undefined;
}
return accessFlags & AccessFlags.Writing
? getIntersectionType(propTypes, aliasSymbol, aliasTypeArguments)
? getIntersectionType(propTypes, IntersectionFlags.None, aliasSymbol, aliasTypeArguments)
: getUnionType(propTypes, UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
}
return getPropertyTypeForIndexType(objectType, apparentObjectType, indexType, indexType, accessNode, accessFlags | AccessFlags.CacheSymbol | AccessFlags.ReportDeprecated);
Expand Down Expand Up @@ -19892,7 +19893,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const newAliasSymbol = aliasSymbol || type.aliasSymbol;
const newAliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(type.aliasTypeArguments, mapper);
return flags & TypeFlags.Intersection || origin && origin.flags & TypeFlags.Intersection ?
getIntersectionType(newTypes, newAliasSymbol, newAliasTypeArguments) :
getIntersectionType(newTypes, IntersectionFlags.None, newAliasSymbol, newAliasTypeArguments) :
getUnionType(newTypes, UnionReduction.Literal, newAliasSymbol, newAliasTypeArguments);
}
if (flags & TypeFlags.Index) {
Expand Down
7 changes: 7 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5377,6 +5377,13 @@ export const enum UnionReduction {
Subtype,
}

/** @internal */
export const enum IntersectionFlags {
None = 0,
NoSupertypeReduction = 1 << 0,
NoConstraintReduction = 1 << 1,
}

// dprint-ignore
/** @internal */
export const enum ContextFlags {
Expand Down
136 changes: 136 additions & 0 deletions tests/baselines/reference/intersectionConstraintReduction.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//// [tests/cases/compiler/intersectionConstraintReduction.ts] ////

=== intersectionConstraintReduction.ts ===
type Test1<K1 extends keyof any, K2 extends keyof any> =
>Test1 : Symbol(Test1, Decl(intersectionConstraintReduction.ts, 0, 0))
>K1 : Symbol(K1, Decl(intersectionConstraintReduction.ts, 0, 11))
>K2 : Symbol(K2, Decl(intersectionConstraintReduction.ts, 0, 32))

MustBeKey<Extract<K1, keyof any> & K1 & K2>;
>MustBeKey : Symbol(MustBeKey, Decl(intersectionConstraintReduction.ts, 4, 48))
>Extract : Symbol(Extract, Decl(lib.es5.d.ts, --, --))
>K1 : Symbol(K1, Decl(intersectionConstraintReduction.ts, 0, 11))
>K1 : Symbol(K1, Decl(intersectionConstraintReduction.ts, 0, 11))
>K2 : Symbol(K2, Decl(intersectionConstraintReduction.ts, 0, 32))

type Test2<K1 extends keyof any, K2 extends keyof any> =
>Test2 : Symbol(Test2, Decl(intersectionConstraintReduction.ts, 1, 48))
>K1 : Symbol(K1, Decl(intersectionConstraintReduction.ts, 3, 11))
>K2 : Symbol(K2, Decl(intersectionConstraintReduction.ts, 3, 32))

MustBeKey<K1 & K2 & Extract<K1, keyof any>>;
>MustBeKey : Symbol(MustBeKey, Decl(intersectionConstraintReduction.ts, 4, 48))
>K1 : Symbol(K1, Decl(intersectionConstraintReduction.ts, 3, 11))
>K2 : Symbol(K2, Decl(intersectionConstraintReduction.ts, 3, 32))
>Extract : Symbol(Extract, Decl(lib.es5.d.ts, --, --))
>K1 : Symbol(K1, Decl(intersectionConstraintReduction.ts, 3, 11))

type MustBeKey<K extends keyof any> = K;
>MustBeKey : Symbol(MustBeKey, Decl(intersectionConstraintReduction.ts, 4, 48))
>K : Symbol(K, Decl(intersectionConstraintReduction.ts, 6, 15))
>K : Symbol(K, Decl(intersectionConstraintReduction.ts, 6, 15))

// https://github.com/microsoft/TypeScript/issues/58370

type AnyKey = number | string | symbol;
>AnyKey : Symbol(AnyKey, Decl(intersectionConstraintReduction.ts, 6, 40))

type ReturnTypeKeyof<Obj extends object> = Obj extends object
>ReturnTypeKeyof : Symbol(ReturnTypeKeyof, Decl(intersectionConstraintReduction.ts, 10, 39))
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 12, 21))
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 12, 21))

? [keyof Obj] extends [never]
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 12, 21))

? never
: { [Key in keyof Obj as string]-?: () => Key }[string]
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 15, 13))
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 12, 21))
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 15, 13))

: never;

type KeyIfSignatureOfObject<
>KeyIfSignatureOfObject : Symbol(KeyIfSignatureOfObject, Decl(intersectionConstraintReduction.ts, 16, 12))

Obj extends object,
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 18, 28))

Key extends AnyKey,
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 19, 23))
>AnyKey : Symbol(AnyKey, Decl(intersectionConstraintReduction.ts, 6, 40))

ReturnTypeKeys = ReturnTypeKeyof<Obj>,
>ReturnTypeKeys : Symbol(ReturnTypeKeys, Decl(intersectionConstraintReduction.ts, 20, 23))
>ReturnTypeKeyof : Symbol(ReturnTypeKeyof, Decl(intersectionConstraintReduction.ts, 10, 39))
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 18, 28))

> = ReturnTypeKeys extends () => Key ? ((() => Key) extends ReturnTypeKeys ? Key : never) : never;
>ReturnTypeKeys : Symbol(ReturnTypeKeys, Decl(intersectionConstraintReduction.ts, 20, 23))
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 19, 23))
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 19, 23))
>ReturnTypeKeys : Symbol(ReturnTypeKeys, Decl(intersectionConstraintReduction.ts, 20, 23))
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 19, 23))

export type Reduced1<Obj extends object, Key extends AnyKey, Value, ObjKeys extends keyof Obj = keyof Obj> =
>Reduced1 : Symbol(Reduced1, Decl(intersectionConstraintReduction.ts, 22, 98))
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 24, 21))
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 24, 40))
>AnyKey : Symbol(AnyKey, Decl(intersectionConstraintReduction.ts, 6, 40))
>Value : Symbol(Value, Decl(intersectionConstraintReduction.ts, 24, 60))
>ObjKeys : Symbol(ObjKeys, Decl(intersectionConstraintReduction.ts, 24, 67))
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 24, 21))
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 24, 21))

Key extends KeyIfSignatureOfObject<Obj, Key>
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 24, 40))
>KeyIfSignatureOfObject : Symbol(KeyIfSignatureOfObject, Decl(intersectionConstraintReduction.ts, 16, 12))
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 24, 21))
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 24, 40))

? Key extends ObjKeys
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 24, 40))
>ObjKeys : Symbol(ObjKeys, Decl(intersectionConstraintReduction.ts, 24, 67))

? { [K in Key]: Value }
>K : Symbol(K, Decl(intersectionConstraintReduction.ts, 27, 17))
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 24, 40))
>Value : Symbol(Value, Decl(intersectionConstraintReduction.ts, 24, 60))

: never
: never;

export type Reduced2<Obj extends object, Key extends AnyKey, Value, ObjKeys extends keyof Obj = keyof Obj> =
>Reduced2 : Symbol(Reduced2, Decl(intersectionConstraintReduction.ts, 29, 16))
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 31, 21))
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 31, 40))
>AnyKey : Symbol(AnyKey, Decl(intersectionConstraintReduction.ts, 6, 40))
>Value : Symbol(Value, Decl(intersectionConstraintReduction.ts, 31, 60))
>ObjKeys : Symbol(ObjKeys, Decl(intersectionConstraintReduction.ts, 31, 67))
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 31, 21))
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 31, 21))

Key extends AnyKey
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 31, 40))
>AnyKey : Symbol(AnyKey, Decl(intersectionConstraintReduction.ts, 6, 40))

? Key extends KeyIfSignatureOfObject<Obj, Key>
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 31, 40))
>KeyIfSignatureOfObject : Symbol(KeyIfSignatureOfObject, Decl(intersectionConstraintReduction.ts, 16, 12))
>Obj : Symbol(Obj, Decl(intersectionConstraintReduction.ts, 31, 21))
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 31, 40))

? Key extends ObjKeys
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 31, 40))
>ObjKeys : Symbol(ObjKeys, Decl(intersectionConstraintReduction.ts, 31, 67))

? { [K in Key]: Value }
>K : Symbol(K, Decl(intersectionConstraintReduction.ts, 35, 20))
>Key : Symbol(Key, Decl(intersectionConstraintReduction.ts, 31, 40))
>Value : Symbol(Value, Decl(intersectionConstraintReduction.ts, 31, 60))

: never
: never
: never;

65 changes: 65 additions & 0 deletions tests/baselines/reference/intersectionConstraintReduction.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//// [tests/cases/compiler/intersectionConstraintReduction.ts] ////

=== intersectionConstraintReduction.ts ===
type Test1<K1 extends keyof any, K2 extends keyof any> =
>Test1 : Extract<K1, string | number | symbol> & K1 & K2
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

MustBeKey<Extract<K1, keyof any> & K1 & K2>;

type Test2<K1 extends keyof any, K2 extends keyof any> =
>Test2 : K1 & K2 & Extract<K1, string | number | symbol>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

MustBeKey<K1 & K2 & Extract<K1, keyof any>>;

type MustBeKey<K extends keyof any> = K;
>MustBeKey : K
> : ^

// https://github.com/microsoft/TypeScript/issues/58370

type AnyKey = number | string | symbol;
>AnyKey : AnyKey
> : ^^^^^^

type ReturnTypeKeyof<Obj extends object> = Obj extends object
>ReturnTypeKeyof : ReturnTypeKeyof<Obj>
> : ^^^^^^^^^^^^^^^^^^^^

? [keyof Obj] extends [never]
? never
: { [Key in keyof Obj as string]-?: () => Key }[string]
: never;

type KeyIfSignatureOfObject<
>KeyIfSignatureOfObject : KeyIfSignatureOfObject<Obj, Key, ReturnTypeKeys>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Obj extends object,
Key extends AnyKey,
ReturnTypeKeys = ReturnTypeKeyof<Obj>,
> = ReturnTypeKeys extends () => Key ? ((() => Key) extends ReturnTypeKeys ? Key : never) : never;

export type Reduced1<Obj extends object, Key extends AnyKey, Value, ObjKeys extends keyof Obj = keyof Obj> =
>Reduced1 : Reduced1<Obj, Key, Value, ObjKeys>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Key extends KeyIfSignatureOfObject<Obj, Key>
? Key extends ObjKeys
? { [K in Key]: Value }
: never
: never;

export type Reduced2<Obj extends object, Key extends AnyKey, Value, ObjKeys extends keyof Obj = keyof Obj> =
>Reduced2 : Reduced2<Obj, Key, Value, ObjKeys>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Key extends AnyKey
? Key extends KeyIfSignatureOfObject<Obj, Key>
? Key extends ObjKeys
? { [K in Key]: Value }
: never
: never
: never;

1 change: 0 additions & 1 deletion tests/baselines/reference/intersectionsOfLargeUnions.types
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//// [tests/cases/compiler/intersectionsOfLargeUnions.ts] ////

=== Performance Stats ===
Strict subtype cache: 1,000
Assignability cache: 1,000
Type Count: 2,500

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//// [tests/cases/compiler/intersectionsOfLargeUnions2.ts] ////

=== Performance Stats ===
Strict subtype cache: 1,000
Assignability cache: 1,000
Type Count: 2,500

Expand Down
Loading