diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 780b120bb7e5f..053cde0e2e448 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1881,6 +1881,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { var tupleTypes = new Map(); var unionTypes = new Map(); + var unionOfUnionTypes = new Map(); var intersectionTypes = new Map(); var stringLiteralTypes = new Map(); var numberLiteralTypes = new Map(); @@ -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; @@ -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]`, + // 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; } @@ -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) {