Skip to content

Optimize union type creation #53771

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
Apr 14, 2023
Merged
Changes from 2 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
33 changes: 29 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1881,6 +1881,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

var tupleTypes = new Map<string, GenericType>();
var unionTypes = new Map<string, UnionType>();
var unionOfUnionTypes = new Map<string, Type>();
var intersectionTypes = new Map<string, Type>();
var stringLiteralTypes = new Map<string, StringLiteralType>();
var numberLiteralTypes = new Map<number, NumberLiteralType>();
Expand Down Expand Up @@ -16234,9 +16235,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

function addTypeToUnion(typeSet: Type[], includes: TypeFlags, type: Type) {
const flags = type.flags;
if (flags & TypeFlags.Union) {
return addTypesToUnion(typeSet, includes | (isNamedUnionType(type) ? TypeFlags.Union : 0), (type as UnionType).types);
}
// We ignore 'never' types in unions
if (!(flags & TypeFlags.Never)) {
includes |= flags & TypeFlags.IncludesMask;
Expand All @@ -16259,8 +16257,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// Add the given types to the given type set. Order is preserved, duplicates are removed,
// and nested types of the given kind are flattened into the set.
function addTypesToUnion(typeSet: Type[], includes: TypeFlags, types: readonly Type[]): TypeFlags {
let lastUnion: Type | undefined;
for (const type of types) {
includes = addTypeToUnion(typeSet, includes, type);
if (type.flags & TypeFlags.Union) {
// We skip the union type if it is the same as the last union we processed. We could potentially track
// all union types that we've processed, but this simple test is fast and covers the scenarios we care
// about (in particular, Record<A, B>[A], where A and B are large union types).
if (type !== lastUnion) {
includes = addTypesToUnion(typeSet, includes | (isNamedUnionType(type) ? TypeFlags.Union : 0), (type as UnionType).types);
lastUnion = type;
}
}
else {
includes = addTypeToUnion(typeSet, includes, type);
}
}
return includes;
}
Expand Down Expand Up @@ -16412,6 +16422,21 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (types.length === 1) {
return types[0];
}
// We optimize for the common case of unioning a union type with some other type (such as `undefined`).
if (types.length === 2 && !origin && (types[0].flags & TypeFlags.Union || types[1].flags & TypeFlags.Union)) {
const infix = unionReduction === UnionReduction.None ? "N" : unionReduction === UnionReduction.Subtype ? "S" : "L";
const id = types[0].id + infix + types[1].id + getAliasId(aliasSymbol, aliasTypeArguments);
let type = unionOfUnionTypes.get(id);
if (!type) {
type = getUnionTypeWorker(types, unionReduction, aliasSymbol, aliasTypeArguments, origin);
unionOfUnionTypes.set(id, type);
}
return type;
}
return getUnionTypeWorker(types, unionReduction, aliasSymbol, aliasTypeArguments, origin);
}

function getUnionTypeWorker(types: readonly Type[], unionReduction: UnionReduction = UnionReduction.Literal, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[], origin?: Type): Type {
let typeSet: Type[] | undefined = [];
const includes = addTypesToUnion(typeSet, 0 as TypeFlags, types);
if (unionReduction !== UnionReduction.None) {
Expand Down