From 44231c80c333b66397c48fc334ac6255148ba03c Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 28 Dec 2020 17:05:44 -1000 Subject: [PATCH] Preserve intersection origin for union types --- src/compiler/checker.ts | 75 +++++++++++++++++++++++------------------ src/compiler/types.ts | 10 ++++-- 2 files changed, 51 insertions(+), 34 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 7d109ee2723..5e91d1cc96e 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4554,14 +4554,16 @@ namespace ts { : factory.createTypeReferenceNode(factory.createIdentifier("?"), /*typeArguments*/ undefined); } if (type.flags & (TypeFlags.Union | TypeFlags.Intersection)) { - const types = type.flags & TypeFlags.Union ? formatUnionTypes((type).originalTypes || (type).types) : (type).types; + const isIntersection = type.flags & TypeFlags.Intersection || (type).origin?.isIntersection; + const types = type.flags & TypeFlags.Intersection ? (type).types : + isIntersection ? (type).origin!.types : + formatUnionTypes((type).origin?.types || (type).types); if (length(types) === 1) { return typeToTypeNodeHelper(types[0], context); } const typeNodes = mapToTypeNodes(types, context, /*isBareList*/ true); if (typeNodes && typeNodes.length > 0) { - const unionOrIntersectionTypeNode = type.flags & TypeFlags.Union ? factory.createUnionTypeNode(typeNodes) : factory.createIntersectionTypeNode(typeNodes); - return unionOrIntersectionTypeNode; + return isIntersection ? factory.createIntersectionTypeNode(typeNodes) : factory.createUnionTypeNode(typeNodes); } else { if (!context.encounteredError && !(context.flags & NodeBuilderFlags.AllowEmptyUnionOrIntersection)) { @@ -11444,7 +11446,7 @@ namespace ts { } function getUnaliasedUnionType(type: Type) { - if (type.flags & TypeFlags.Union && !(type.flags & TypeFlags.EnumLiteral) && (type).aliasSymbol) { + if (type.flags & TypeFlags.Union && !(type.flags & TypeFlags.EnumLiteral) && ((type).aliasSymbol || (type).origin)) { return (type).unaliasedType || ((type).unaliasedType = getUnionTypeFromSortedList((type).types, (type).objectFlags)); } return type; @@ -13249,20 +13251,6 @@ namespace ts { } } - function reduceOriginalTypes(types: readonly Type[], typeSet: readonly Type[]) { - const aliasedUnions = filter(types, t => !!(t.flags & TypeFlags.Union && t.aliasSymbol)); - const result: Type[] = []; - for (const t of typeSet) { - if (!(t.flags & TypeFlags.Union && t.aliasSymbol) && !some(aliasedUnions, union => containsType((union).types, t))) { - result.push(t); - } - } - for (const t of aliasedUnions) { - insertType(result, t); - } - return result; - } - // We sort and deduplicate the constituent types based on object identity. If the subtypeReduction // flag is specified we also reduce the constituent type set to only include types that aren't subtypes // of other types. Subtype reduction is expensive for large union types and is possible only when union @@ -13270,7 +13258,7 @@ namespace ts { // expression constructs such as array literals and the || and ?: operators). Named types can // circularly reference themselves and therefore cannot be subtype reduced during their declaration. // For example, "type Item = string | (() => Item" is a named type that circularly references itself. - function getUnionType(types: readonly Type[], unionReduction: UnionReduction = UnionReduction.Literal, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type { + function getUnionType(types: readonly Type[], unionReduction: UnionReduction = UnionReduction.Literal, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[], origin?: UnionOrigin): Type { if (types.length === 0) { return neverType; } @@ -13279,10 +13267,6 @@ namespace ts { } const typeSet: Type[] = []; const includes = addTypesToUnion(typeSet, 0, types); - const originalTypes = includes & TypeFlags.Union ? reduceOriginalTypes(types, typeSet) : undefined; - if (!aliasSymbol && originalTypes && originalTypes.length === 1) { - return originalTypes[0]; - } if (unionReduction !== UnionReduction.None) { if (includes & TypeFlags.AnyOrUnknown) { return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : unknownType; @@ -13308,9 +13292,27 @@ namespace ts { neverType; } } + if (!origin && includes & TypeFlags.Union) { + const aliasedUnions = filter(types, t => !!(t.flags & TypeFlags.Union && t.aliasSymbol)); + const reducedTypes: Type[] = []; + for (const t of typeSet) { + if (!(t.flags & TypeFlags.Union && t.aliasSymbol) && !some(aliasedUnions, union => containsType((union).types, t))) { + reducedTypes.push(t); + } + } + for (const t of aliasedUnions) { + insertType(reducedTypes, t); + } + if (!aliasSymbol && reducedTypes.length === 1) { + return reducedTypes[0]; + } + if (reducedTypes.length <= typeSet.length) { + origin = { types: reducedTypes, isIntersection: false }; + } + } const objectFlags = (includes & TypeFlags.NotPrimitiveUnion ? 0 : ObjectFlags.PrimitiveUnion) | (includes & TypeFlags.Intersection ? ObjectFlags.ContainsIntersections : 0); - return getUnionTypeFromSortedList(typeSet, objectFlags, originalTypes && originalTypes.length <= typeSet.length ? originalTypes : undefined, aliasSymbol, aliasTypeArguments); + return getUnionTypeFromSortedList(typeSet, objectFlags, aliasSymbol, aliasTypeArguments, origin); } function getUnionTypePredicate(signatures: readonly Signature[]): TypePredicate | undefined { @@ -13346,21 +13348,21 @@ namespace ts { } // This function assumes the constituent type list is sorted and deduplicated. - function getUnionTypeFromSortedList(types: Type[], objectFlags: ObjectFlags, originalTypes?: Type[], aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type { + function getUnionTypeFromSortedList(types: Type[], objectFlags: ObjectFlags, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[], origin?: UnionOrigin): Type { if (types.length === 0) { return neverType; } if (types.length === 1) { return types[0]; } - const id = getTypeListId(types) + (originalTypes ? `#${getTypeListId(originalTypes)}` : "") + (aliasSymbol ? `@${getSymbolId(aliasSymbol)}` : ""); + const id = getTypeListId(types) + (origin ? `${origin.isIntersection ? '&' : '|'}${getTypeListId(origin.types)}` : "") + (aliasSymbol ? `@${getSymbolId(aliasSymbol)}` : ""); let type = unionTypes.get(id); if (!type) { type = createType(TypeFlags.Union); unionTypes.set(id, type); type.objectFlags = objectFlags | getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable); type.types = types; - type.originalTypes = originalTypes; + type.origin = origin; /* Note: This is the alias symbol (or lack thereof) that we see when we first encounter this union type. For aliases of identical unions, eg `type T = A | B; type U = A | B`, the symbol of the first alias encountered is the aliasSymbol. @@ -13611,10 +13613,10 @@ namespace ts { result = getIntersectionType(typeSet, aliasSymbol, aliasTypeArguments); } else if (extractIrreducible(typeSet, TypeFlags.Undefined)) { - result = getUnionType([getIntersectionType(typeSet), undefinedType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments); + result = getUnionTypeFromIntersection([getIntersectionType(typeSet), undefinedType], aliasSymbol, aliasTypeArguments, typeSet); } else if (extractIrreducible(typeSet, TypeFlags.Null)) { - result = getUnionType([getIntersectionType(typeSet), nullType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments); + result = getUnionTypeFromIntersection([getIntersectionType(typeSet), nullType], aliasSymbol, aliasTypeArguments, typeSet); } else { // We are attempting to construct a type of the form X & (A | B) & Y. Transform this into a type of @@ -13625,8 +13627,8 @@ namespace ts { } const unionIndex = findIndex(typeSet, t => (t.flags & TypeFlags.Union) !== 0); const unionType = typeSet[unionIndex]; - result = getUnionType(map(unionType.types, t => getIntersectionType(replaceElement(typeSet, unionIndex, t))), - UnionReduction.Literal, aliasSymbol, aliasTypeArguments); + result = getUnionTypeFromIntersection(map(unionType.types, t => getIntersectionType(replaceElement(typeSet, unionIndex, t))), + aliasSymbol, aliasTypeArguments, typeSet); } } else { @@ -13637,6 +13639,11 @@ namespace ts { return result; } + function getUnionTypeFromIntersection(types: readonly Type[], aliasSymbol: Symbol | undefined, aliasTypeArguments: readonly Type[] | undefined, originalTypes: readonly Type[]) { + const origin = some(types, t => !!(t.flags & TypeFlags.Intersection)) ? { types: originalTypes, isIntersection: true } : undefined; + return getUnionType(types, UnionReduction.Literal, aliasSymbol, aliasTypeArguments, origin); + } + function checkCrossProductUnion(types: readonly Type[]) { const size = reduceLeft(types, (n, t) => n * (t.flags & TypeFlags.Union ? (t).types.length : t.flags & TypeFlags.Never ? 0 : 1), 1); if (size >= 100000) { @@ -15507,6 +15514,10 @@ namespace ts { return getConditionalType(root, mapper); } + function instantiateUnionOrigin(origin: UnionOrigin | undefined, mapper: TypeMapper) { + return origin && { types: instantiateTypes(origin.types, mapper), isIntersection: origin.isIntersection }; + } + function instantiateType(type: Type, mapper: TypeMapper | undefined): Type; function instantiateType(type: Type | undefined, mapper: TypeMapper | undefined): Type | undefined; function instantiateType(type: Type | undefined, mapper: TypeMapper | undefined): Type | undefined { @@ -15552,7 +15563,7 @@ namespace ts { return newTypes === types ? type : flags & TypeFlags.Intersection ? getIntersectionType(newTypes, type.aliasSymbol, instantiateTypes(type.aliasTypeArguments, mapper)) : - getUnionType(newTypes, UnionReduction.Literal, type.aliasSymbol, instantiateTypes(type.aliasTypeArguments, mapper)); + getUnionType(newTypes, UnionReduction.Literal, type.aliasSymbol, instantiateTypes(type.aliasTypeArguments, mapper), instantiateUnionOrigin((type).origin, mapper)); } if (flags & TypeFlags.Index) { return getIndexType(instantiateType((type).type, mapper)); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 0b88a3f606e..dab3f115372 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5288,8 +5288,6 @@ namespace ts { resolvedStringIndexType: IndexType; /* @internal */ resolvedBaseConstraint: Type; - /* @internal */ - originalTypes?: Type[]; } export interface UnionType extends UnionOrIntersectionType { @@ -5299,6 +5297,14 @@ namespace ts { regularType?: UnionType; /* @internal */ unaliasedType?: Type; + /* @internal */ + origin?: UnionOrigin; + } + + /* @internal */ + export interface UnionOrigin { + types: readonly Type[]; + isIntersection: boolean; } export interface IntersectionType extends UnionOrIntersectionType {