From 111feca5e574c249608d7166ec2284f3da9eb6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 5 Mar 2025 09:47:06 +0100 Subject: [PATCH] Improve union origin preservation in filtering-unionizing binary expressions --- src/compiler/checker.ts | 34 +++++--- src/compiler/types.ts | 2 + ...ionBinaryExpressionPreserveOrigin1.symbols | 62 ++++++++++++++ ...unionBinaryExpressionPreserveOrigin1.types | 81 +++++++++++++++++++ .../unionBinaryExpressionPreserveOrigin1.ts | 20 +++++ 5 files changed, 189 insertions(+), 10 deletions(-) create mode 100644 tests/baselines/reference/unionBinaryExpressionPreserveOrigin1.symbols create mode 100644 tests/baselines/reference/unionBinaryExpressionPreserveOrigin1.types create mode 100644 tests/cases/compiler/unionBinaryExpressionPreserveOrigin1.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index caab75c99fa8a..eff6997e16d84 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -17409,7 +17409,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return false; } - function addTypeToUnion(typeSet: Type[], includes: TypeFlags, type: Type) { + function addTypeToUnion(typeSet: Type[] | undefined, includes: TypeFlags, type: Type) { const flags = type.flags; // We ignore 'never' types in unions if (!(flags & TypeFlags.Never)) { @@ -17421,7 +17421,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (!strictNullChecks && flags & TypeFlags.Nullable) { if (!(getObjectFlags(type) & ObjectFlags.ContainsWideningType)) includes |= TypeFlags.IncludesNonWideningType; } - else { + else if (typeSet) { const len = typeSet.length; const index = len && type.id > typeSet[len - 1].id ? ~len : binarySearch(typeSet, type, getTypeId, compareValues); if (index < 0) { @@ -17434,7 +17434,7 @@ 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 { + function addTypesToUnion(typeSet: Type[] | undefined, includes: TypeFlags, types: readonly Type[]): TypeFlags { let lastType: Type | undefined; for (const type of types) { // We skip the type if it is the same as the last type we processed. This simple test particularly @@ -19644,6 +19644,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return !!(type.flags & TypeFlags.Freshable) && (type as LiteralType).freshType === type; } + function isRegularLiteralType(type: Type) { + return !!(type.flags & TypeFlags.Freshable) && (type as LiteralType).regularType === type; + } + function getStringLiteralType(value: string): StringLiteralType { let type; return stringLiteralTypes.get(value) || @@ -25036,10 +25040,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return filterType(type, t => hasTypeFacts(t, TypeFacts.Truthy)); } - function extractDefinitelyFalsyTypes(type: Type): Type { - return mapType(type, getDefinitelyFalsyPartOfType); - } - function getDefinitelyFalsyPartOfType(type: Type): Type { return type.flags & TypeFlags.String ? emptyStringType : type.flags & TypeFlags.Number ? zeroType : @@ -40160,7 +40160,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { case SyntaxKind.AmpersandAmpersandToken: case SyntaxKind.AmpersandAmpersandEqualsToken: { const resultType = hasTypeFacts(leftType, TypeFacts.Truthy) ? - getUnionType([extractDefinitelyFalsyTypes(strictNullChecks ? leftType : getBaseTypeOfLiteralType(rightType)), rightType]) : + getUnionOfLeftAndRightTypes(strictNullChecks ? leftType : getBaseTypeOfLiteralType(rightType), rightType, getDefinitelyFalsyPartOfType) : leftType; if (operator === SyntaxKind.AmpersandAmpersandEqualsToken) { checkAssignmentOperator(rightType); @@ -40170,7 +40170,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { case SyntaxKind.BarBarToken: case SyntaxKind.BarBarEqualsToken: { const resultType = hasTypeFacts(leftType, TypeFacts.Falsy) ? - getUnionType([getNonNullableType(removeDefinitelyFalsyTypes(leftType)), rightType], UnionReduction.Subtype) : + getUnionOfLeftAndRightTypes(leftType, rightType, t => hasTypeFacts(t, TypeFacts.Truthy) ? getNonNullableType(t) : neverType, UnionReduction.Subtype) : leftType; if (operator === SyntaxKind.BarBarEqualsToken) { checkAssignmentOperator(rightType); @@ -40180,7 +40180,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { case SyntaxKind.QuestionQuestionToken: case SyntaxKind.QuestionQuestionEqualsToken: { const resultType = hasTypeFacts(leftType, TypeFacts.EQUndefinedOrNull) ? - getUnionType([getNonNullableType(leftType), rightType], UnionReduction.Subtype) : + getUnionOfLeftAndRightTypes(leftType, rightType, getNonNullableType, UnionReduction.Subtype) : leftType; if (operator === SyntaxKind.QuestionQuestionEqualsToken) { checkAssignmentOperator(rightType); @@ -40415,6 +40415,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } return false; } + + function getUnionOfLeftAndRightTypes(leftType: Type, rightType: Type, adjustLeft: (type: Type) => Type, unionReduction?: UnionReduction) { + const rightTypes = rightType.flags & TypeFlags.Union ? (rightType as UnionType).types : [rightType]; + const includes = addTypesToUnion(/*typeSet*/ undefined, 0 as TypeFlags, rightTypes) & (TypeFlags.BaseOfLiteral | TypeFlags.Nullable); + return getUnionType([ + mapType( + leftType, + // when something could be removed from the left type and when it's in the right type it means it would be re-added right away + // in such a case it's preserved in the mapped left type to help with origin/alias preservation + t => includes & t.flags || isRegularLiteralType(t) && containsType(rightTypes, (t as LiteralType).freshType) ? t : adjustLeft(t), + ), + rightType, + ], unionReduction); + } } function getBaseTypesIfUnrelated(leftType: Type, rightType: Type, isRelated: (left: Type, right: Type) => boolean): [Type, Type] { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f6ca75a68e17d..e8310d15c621a 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6306,6 +6306,8 @@ export const enum TypeFlags { /** @internal */ Nullable = Undefined | Null, Literal = StringLiteral | NumberLiteral | BigIntLiteral | BooleanLiteral, + /** @internal */ + BaseOfLiteral = String | Number | BigInt | Boolean, Unit = Enum | Literal | UniqueESSymbol | Nullable, Freshable = Enum | Literal, StringOrNumberLiteral = StringLiteral | NumberLiteral, diff --git a/tests/baselines/reference/unionBinaryExpressionPreserveOrigin1.symbols b/tests/baselines/reference/unionBinaryExpressionPreserveOrigin1.symbols new file mode 100644 index 0000000000000..4ce04e77811ea --- /dev/null +++ b/tests/baselines/reference/unionBinaryExpressionPreserveOrigin1.symbols @@ -0,0 +1,62 @@ +//// [tests/cases/compiler/unionBinaryExpressionPreserveOrigin1.ts] //// + +=== unionBinaryExpressionPreserveOrigin1.ts === +// https://github.com/microsoft/TypeScript/issues/43031 + +type Brand = K & { __brand: T }; +>Brand : Symbol(Brand, Decl(unionBinaryExpressionPreserveOrigin1.ts, 0, 0)) +>K : Symbol(K, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 11)) +>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 13)) +>K : Symbol(K, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 11)) +>__brand : Symbol(__brand, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 24)) +>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 13)) + +type BrandedUnknown = Brand<"unknown", T>; +>BrandedUnknown : Symbol(BrandedUnknown, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 38)) +>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 3, 20)) +>Brand : Symbol(Brand, Decl(unionBinaryExpressionPreserveOrigin1.ts, 0, 0)) +>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 3, 20)) + +type Maybe = T | BrandedUnknown; +>Maybe : Symbol(Maybe, Decl(unionBinaryExpressionPreserveOrigin1.ts, 3, 45)) +>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 4, 11)) +>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 4, 11)) +>BrandedUnknown : Symbol(BrandedUnknown, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 38)) +>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 4, 11)) + +declare const m1: Maybe | undefined; +>m1 : Symbol(m1, Decl(unionBinaryExpressionPreserveOrigin1.ts, 6, 13)) +>Maybe : Symbol(Maybe, Decl(unionBinaryExpressionPreserveOrigin1.ts, 3, 45)) + +const test1 = m1 || false; +>test1 : Symbol(test1, Decl(unionBinaryExpressionPreserveOrigin1.ts, 7, 5)) +>m1 : Symbol(m1, Decl(unionBinaryExpressionPreserveOrigin1.ts, 6, 13)) + +const test2 = m1 ?? false; +>test2 : Symbol(test2, Decl(unionBinaryExpressionPreserveOrigin1.ts, 8, 5)) +>m1 : Symbol(m1, Decl(unionBinaryExpressionPreserveOrigin1.ts, 6, 13)) + +declare const m2: Maybe | undefined; +>m2 : Symbol(m2, Decl(unionBinaryExpressionPreserveOrigin1.ts, 10, 13)) +>Maybe : Symbol(Maybe, Decl(unionBinaryExpressionPreserveOrigin1.ts, 3, 45)) + +const test3 = m2 || null; +>test3 : Symbol(test3, Decl(unionBinaryExpressionPreserveOrigin1.ts, 11, 5)) +>m2 : Symbol(m2, Decl(unionBinaryExpressionPreserveOrigin1.ts, 10, 13)) + +const test4 = m2 ?? null; +>test4 : Symbol(test4, Decl(unionBinaryExpressionPreserveOrigin1.ts, 12, 5)) +>m2 : Symbol(m2, Decl(unionBinaryExpressionPreserveOrigin1.ts, 10, 13)) + +type StrOrNum = string | number +>StrOrNum : Symbol(StrOrNum, Decl(unionBinaryExpressionPreserveOrigin1.ts, 12, 25)) + +declare const numOrStr: StrOrNum; +>numOrStr : Symbol(numOrStr, Decl(unionBinaryExpressionPreserveOrigin1.ts, 15, 13)) +>StrOrNum : Symbol(StrOrNum, Decl(unionBinaryExpressionPreserveOrigin1.ts, 12, 25)) + +const test5 = numOrStr && numOrStr; +>test5 : Symbol(test5, Decl(unionBinaryExpressionPreserveOrigin1.ts, 16, 5)) +>numOrStr : Symbol(numOrStr, Decl(unionBinaryExpressionPreserveOrigin1.ts, 15, 13)) +>numOrStr : Symbol(numOrStr, Decl(unionBinaryExpressionPreserveOrigin1.ts, 15, 13)) + diff --git a/tests/baselines/reference/unionBinaryExpressionPreserveOrigin1.types b/tests/baselines/reference/unionBinaryExpressionPreserveOrigin1.types new file mode 100644 index 0000000000000..bc4028eb51437 --- /dev/null +++ b/tests/baselines/reference/unionBinaryExpressionPreserveOrigin1.types @@ -0,0 +1,81 @@ +//// [tests/cases/compiler/unionBinaryExpressionPreserveOrigin1.ts] //// + +=== unionBinaryExpressionPreserveOrigin1.ts === +// https://github.com/microsoft/TypeScript/issues/43031 + +type Brand = K & { __brand: T }; +>Brand : Brand +> : ^^^^^^^^^^^ +>__brand : T +> : ^ + +type BrandedUnknown = Brand<"unknown", T>; +>BrandedUnknown : BrandedUnknown +> : ^^^^^^^^^^^^^^^^^ + +type Maybe = T | BrandedUnknown; +>Maybe : Maybe +> : ^^^^^^^^ + +declare const m1: Maybe | undefined; +>m1 : Maybe | undefined +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +const test1 = m1 || false; +>test1 : Maybe +> : ^^^^^^^^^^^^^^ +>m1 || false : Maybe +> : ^^^^^^^^^^^^^^ +>m1 : Maybe | undefined +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^ +>false : false +> : ^^^^^ + +const test2 = m1 ?? false; +>test2 : Maybe +> : ^^^^^^^^^^^^^^ +>m1 ?? false : Maybe +> : ^^^^^^^^^^^^^^ +>m1 : Maybe | undefined +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^ +>false : false +> : ^^^^^ + +declare const m2: Maybe | undefined; +>m2 : Maybe | undefined +> : ^^^^^^^^^^^^^^^^^^^^^^^ + +const test3 = m2 || null; +>test3 : Maybe +> : ^^^^^^^^^^^ +>m2 || null : Maybe +> : ^^^^^^^^^^^ +>m2 : Maybe | undefined +> : ^^^^^^^^^^^^^^^^^^^^^^^ + +const test4 = m2 ?? null; +>test4 : Maybe +> : ^^^^^^^^^^^ +>m2 ?? null : Maybe +> : ^^^^^^^^^^^ +>m2 : Maybe | undefined +> : ^^^^^^^^^^^^^^^^^^^^^^^ + +type StrOrNum = string | number +>StrOrNum : StrOrNum +> : ^^^^^^^^ + +declare const numOrStr: StrOrNum; +>numOrStr : StrOrNum +> : ^^^^^^^^ + +const test5 = numOrStr && numOrStr; +>test5 : StrOrNum +> : ^^^^^^^^ +>numOrStr && numOrStr : StrOrNum +> : ^^^^^^^^ +>numOrStr : StrOrNum +> : ^^^^^^^^ +>numOrStr : StrOrNum +> : ^^^^^^^^ + diff --git a/tests/cases/compiler/unionBinaryExpressionPreserveOrigin1.ts b/tests/cases/compiler/unionBinaryExpressionPreserveOrigin1.ts new file mode 100644 index 0000000000000..d25480364cc35 --- /dev/null +++ b/tests/cases/compiler/unionBinaryExpressionPreserveOrigin1.ts @@ -0,0 +1,20 @@ +// @strict: true +// @noEmit: true + +// https://github.com/microsoft/TypeScript/issues/43031 + +type Brand = K & { __brand: T }; +type BrandedUnknown = Brand<"unknown", T>; +type Maybe = T | BrandedUnknown; + +declare const m1: Maybe | undefined; +const test1 = m1 || false; +const test2 = m1 ?? false; + +declare const m2: Maybe | undefined; +const test3 = m2 || null; +const test4 = m2 ?? null; + +type StrOrNum = string | number +declare const numOrStr: StrOrNum; +const test5 = numOrStr && numOrStr;