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 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: 27 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,17 @@ 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 lastType: Type | undefined;
for (const type of types) {
includes = addTypeToUnion(typeSet, includes, type);
// We skip the type if it is the same as the last type we processed. This simple test particularly
// saves a lot of work for large lists of the same union type, such as when resolving `Record<A, B>[A]`,
// where A and B are large union types.
if (type !== lastType) {
includes = type.flags & TypeFlags.Union ?
addTypesToUnion(typeSet, includes | (isNamedUnionType(type) ? TypeFlags.Union : 0), (type as UnionType).types) :
addTypeToUnion(typeSet, includes, type);
lastType = type;
}
}
return includes;
}
Expand Down Expand Up @@ -16412,6 +16419,22 @@ 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 index = types[0].id < types[1].id ? 0 : 1;
const id = types[index].id + infix + types[1 - index].id + getAliasId(aliasSymbol, aliasTypeArguments);
let type = unionOfUnionTypes.get(id);
if (!type) {
type = getUnionTypeWorker(types, unionReduction, aliasSymbol, aliasTypeArguments, /*origin*/ undefined);
unionOfUnionTypes.set(id, type);
}
return type;
}
return getUnionTypeWorker(types, unionReduction, aliasSymbol, aliasTypeArguments, origin);
}

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