Skip to content

Commit 111feca

Browse files
committed
Improve union origin preservation in filtering-unionizing binary expressions
1 parent c85e626 commit 111feca

File tree

5 files changed

+189
-10
lines changed

5 files changed

+189
-10
lines changed

src/compiler/checker.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17409,7 +17409,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1740917409
return false;
1741017410
}
1741117411

17412-
function addTypeToUnion(typeSet: Type[], includes: TypeFlags, type: Type) {
17412+
function addTypeToUnion(typeSet: Type[] | undefined, includes: TypeFlags, type: Type) {
1741317413
const flags = type.flags;
1741417414
// We ignore 'never' types in unions
1741517415
if (!(flags & TypeFlags.Never)) {
@@ -17421,7 +17421,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1742117421
if (!strictNullChecks && flags & TypeFlags.Nullable) {
1742217422
if (!(getObjectFlags(type) & ObjectFlags.ContainsWideningType)) includes |= TypeFlags.IncludesNonWideningType;
1742317423
}
17424-
else {
17424+
else if (typeSet) {
1742517425
const len = typeSet.length;
1742617426
const index = len && type.id > typeSet[len - 1].id ? ~len : binarySearch(typeSet, type, getTypeId, compareValues);
1742717427
if (index < 0) {
@@ -17434,7 +17434,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1743417434

1743517435
// Add the given types to the given type set. Order is preserved, duplicates are removed,
1743617436
// and nested types of the given kind are flattened into the set.
17437-
function addTypesToUnion(typeSet: Type[], includes: TypeFlags, types: readonly Type[]): TypeFlags {
17437+
function addTypesToUnion(typeSet: Type[] | undefined, includes: TypeFlags, types: readonly Type[]): TypeFlags {
1743817438
let lastType: Type | undefined;
1743917439
for (const type of types) {
1744017440
// 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 {
1964419644
return !!(type.flags & TypeFlags.Freshable) && (type as LiteralType).freshType === type;
1964519645
}
1964619646

19647+
function isRegularLiteralType(type: Type) {
19648+
return !!(type.flags & TypeFlags.Freshable) && (type as LiteralType).regularType === type;
19649+
}
19650+
1964719651
function getStringLiteralType(value: string): StringLiteralType {
1964819652
let type;
1964919653
return stringLiteralTypes.get(value) ||
@@ -25036,10 +25040,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2503625040
return filterType(type, t => hasTypeFacts(t, TypeFacts.Truthy));
2503725041
}
2503825042

25039-
function extractDefinitelyFalsyTypes(type: Type): Type {
25040-
return mapType(type, getDefinitelyFalsyPartOfType);
25041-
}
25042-
2504325043
function getDefinitelyFalsyPartOfType(type: Type): Type {
2504425044
return type.flags & TypeFlags.String ? emptyStringType :
2504525045
type.flags & TypeFlags.Number ? zeroType :
@@ -40160,7 +40160,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4016040160
case SyntaxKind.AmpersandAmpersandToken:
4016140161
case SyntaxKind.AmpersandAmpersandEqualsToken: {
4016240162
const resultType = hasTypeFacts(leftType, TypeFacts.Truthy) ?
40163-
getUnionType([extractDefinitelyFalsyTypes(strictNullChecks ? leftType : getBaseTypeOfLiteralType(rightType)), rightType]) :
40163+
getUnionOfLeftAndRightTypes(strictNullChecks ? leftType : getBaseTypeOfLiteralType(rightType), rightType, getDefinitelyFalsyPartOfType) :
4016440164
leftType;
4016540165
if (operator === SyntaxKind.AmpersandAmpersandEqualsToken) {
4016640166
checkAssignmentOperator(rightType);
@@ -40170,7 +40170,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4017040170
case SyntaxKind.BarBarToken:
4017140171
case SyntaxKind.BarBarEqualsToken: {
4017240172
const resultType = hasTypeFacts(leftType, TypeFacts.Falsy) ?
40173-
getUnionType([getNonNullableType(removeDefinitelyFalsyTypes(leftType)), rightType], UnionReduction.Subtype) :
40173+
getUnionOfLeftAndRightTypes(leftType, rightType, t => hasTypeFacts(t, TypeFacts.Truthy) ? getNonNullableType(t) : neverType, UnionReduction.Subtype) :
4017440174
leftType;
4017540175
if (operator === SyntaxKind.BarBarEqualsToken) {
4017640176
checkAssignmentOperator(rightType);
@@ -40180,7 +40180,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4018040180
case SyntaxKind.QuestionQuestionToken:
4018140181
case SyntaxKind.QuestionQuestionEqualsToken: {
4018240182
const resultType = hasTypeFacts(leftType, TypeFacts.EQUndefinedOrNull) ?
40183-
getUnionType([getNonNullableType(leftType), rightType], UnionReduction.Subtype) :
40183+
getUnionOfLeftAndRightTypes(leftType, rightType, getNonNullableType, UnionReduction.Subtype) :
4018440184
leftType;
4018540185
if (operator === SyntaxKind.QuestionQuestionEqualsToken) {
4018640186
checkAssignmentOperator(rightType);
@@ -40415,6 +40415,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4041540415
}
4041640416
return false;
4041740417
}
40418+
40419+
function getUnionOfLeftAndRightTypes(leftType: Type, rightType: Type, adjustLeft: (type: Type) => Type, unionReduction?: UnionReduction) {
40420+
const rightTypes = rightType.flags & TypeFlags.Union ? (rightType as UnionType).types : [rightType];
40421+
const includes = addTypesToUnion(/*typeSet*/ undefined, 0 as TypeFlags, rightTypes) & (TypeFlags.BaseOfLiteral | TypeFlags.Nullable);
40422+
return getUnionType([
40423+
mapType(
40424+
leftType,
40425+
// 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
40426+
// in such a case it's preserved in the mapped left type to help with origin/alias preservation
40427+
t => includes & t.flags || isRegularLiteralType(t) && containsType(rightTypes, (t as LiteralType).freshType) ? t : adjustLeft(t),
40428+
),
40429+
rightType,
40430+
], unionReduction);
40431+
}
4041840432
}
4041940433

4042040434
function getBaseTypesIfUnrelated(leftType: Type, rightType: Type, isRelated: (left: Type, right: Type) => boolean): [Type, Type] {

src/compiler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6306,6 +6306,8 @@ export const enum TypeFlags {
63066306
/** @internal */
63076307
Nullable = Undefined | Null,
63086308
Literal = StringLiteral | NumberLiteral | BigIntLiteral | BooleanLiteral,
6309+
/** @internal */
6310+
BaseOfLiteral = String | Number | BigInt | Boolean,
63096311
Unit = Enum | Literal | UniqueESSymbol | Nullable,
63106312
Freshable = Enum | Literal,
63116313
StringOrNumberLiteral = StringLiteral | NumberLiteral,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//// [tests/cases/compiler/unionBinaryExpressionPreserveOrigin1.ts] ////
2+
3+
=== unionBinaryExpressionPreserveOrigin1.ts ===
4+
// https://github.com/microsoft/TypeScript/issues/43031
5+
6+
type Brand<K, T> = K & { __brand: T };
7+
>Brand : Symbol(Brand, Decl(unionBinaryExpressionPreserveOrigin1.ts, 0, 0))
8+
>K : Symbol(K, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 11))
9+
>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 13))
10+
>K : Symbol(K, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 11))
11+
>__brand : Symbol(__brand, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 24))
12+
>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 13))
13+
14+
type BrandedUnknown<T> = Brand<"unknown", T>;
15+
>BrandedUnknown : Symbol(BrandedUnknown, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 38))
16+
>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 3, 20))
17+
>Brand : Symbol(Brand, Decl(unionBinaryExpressionPreserveOrigin1.ts, 0, 0))
18+
>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 3, 20))
19+
20+
type Maybe<T> = T | BrandedUnknown<T>;
21+
>Maybe : Symbol(Maybe, Decl(unionBinaryExpressionPreserveOrigin1.ts, 3, 45))
22+
>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 4, 11))
23+
>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 4, 11))
24+
>BrandedUnknown : Symbol(BrandedUnknown, Decl(unionBinaryExpressionPreserveOrigin1.ts, 2, 38))
25+
>T : Symbol(T, Decl(unionBinaryExpressionPreserveOrigin1.ts, 4, 11))
26+
27+
declare const m1: Maybe<boolean> | undefined;
28+
>m1 : Symbol(m1, Decl(unionBinaryExpressionPreserveOrigin1.ts, 6, 13))
29+
>Maybe : Symbol(Maybe, Decl(unionBinaryExpressionPreserveOrigin1.ts, 3, 45))
30+
31+
const test1 = m1 || false;
32+
>test1 : Symbol(test1, Decl(unionBinaryExpressionPreserveOrigin1.ts, 7, 5))
33+
>m1 : Symbol(m1, Decl(unionBinaryExpressionPreserveOrigin1.ts, 6, 13))
34+
35+
const test2 = m1 ?? false;
36+
>test2 : Symbol(test2, Decl(unionBinaryExpressionPreserveOrigin1.ts, 8, 5))
37+
>m1 : Symbol(m1, Decl(unionBinaryExpressionPreserveOrigin1.ts, 6, 13))
38+
39+
declare const m2: Maybe<null> | undefined;
40+
>m2 : Symbol(m2, Decl(unionBinaryExpressionPreserveOrigin1.ts, 10, 13))
41+
>Maybe : Symbol(Maybe, Decl(unionBinaryExpressionPreserveOrigin1.ts, 3, 45))
42+
43+
const test3 = m2 || null;
44+
>test3 : Symbol(test3, Decl(unionBinaryExpressionPreserveOrigin1.ts, 11, 5))
45+
>m2 : Symbol(m2, Decl(unionBinaryExpressionPreserveOrigin1.ts, 10, 13))
46+
47+
const test4 = m2 ?? null;
48+
>test4 : Symbol(test4, Decl(unionBinaryExpressionPreserveOrigin1.ts, 12, 5))
49+
>m2 : Symbol(m2, Decl(unionBinaryExpressionPreserveOrigin1.ts, 10, 13))
50+
51+
type StrOrNum = string | number
52+
>StrOrNum : Symbol(StrOrNum, Decl(unionBinaryExpressionPreserveOrigin1.ts, 12, 25))
53+
54+
declare const numOrStr: StrOrNum;
55+
>numOrStr : Symbol(numOrStr, Decl(unionBinaryExpressionPreserveOrigin1.ts, 15, 13))
56+
>StrOrNum : Symbol(StrOrNum, Decl(unionBinaryExpressionPreserveOrigin1.ts, 12, 25))
57+
58+
const test5 = numOrStr && numOrStr;
59+
>test5 : Symbol(test5, Decl(unionBinaryExpressionPreserveOrigin1.ts, 16, 5))
60+
>numOrStr : Symbol(numOrStr, Decl(unionBinaryExpressionPreserveOrigin1.ts, 15, 13))
61+
>numOrStr : Symbol(numOrStr, Decl(unionBinaryExpressionPreserveOrigin1.ts, 15, 13))
62+
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//// [tests/cases/compiler/unionBinaryExpressionPreserveOrigin1.ts] ////
2+
3+
=== unionBinaryExpressionPreserveOrigin1.ts ===
4+
// https://github.com/microsoft/TypeScript/issues/43031
5+
6+
type Brand<K, T> = K & { __brand: T };
7+
>Brand : Brand<K, T>
8+
> : ^^^^^^^^^^^
9+
>__brand : T
10+
> : ^
11+
12+
type BrandedUnknown<T> = Brand<"unknown", T>;
13+
>BrandedUnknown : BrandedUnknown<T>
14+
> : ^^^^^^^^^^^^^^^^^
15+
16+
type Maybe<T> = T | BrandedUnknown<T>;
17+
>Maybe : Maybe<T>
18+
> : ^^^^^^^^
19+
20+
declare const m1: Maybe<boolean> | undefined;
21+
>m1 : Maybe<boolean> | undefined
22+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^
23+
24+
const test1 = m1 || false;
25+
>test1 : Maybe<boolean>
26+
> : ^^^^^^^^^^^^^^
27+
>m1 || false : Maybe<boolean>
28+
> : ^^^^^^^^^^^^^^
29+
>m1 : Maybe<boolean> | undefined
30+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^
31+
>false : false
32+
> : ^^^^^
33+
34+
const test2 = m1 ?? false;
35+
>test2 : Maybe<boolean>
36+
> : ^^^^^^^^^^^^^^
37+
>m1 ?? false : Maybe<boolean>
38+
> : ^^^^^^^^^^^^^^
39+
>m1 : Maybe<boolean> | undefined
40+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^
41+
>false : false
42+
> : ^^^^^
43+
44+
declare const m2: Maybe<null> | undefined;
45+
>m2 : Maybe<null> | undefined
46+
> : ^^^^^^^^^^^^^^^^^^^^^^^
47+
48+
const test3 = m2 || null;
49+
>test3 : Maybe<null>
50+
> : ^^^^^^^^^^^
51+
>m2 || null : Maybe<null>
52+
> : ^^^^^^^^^^^
53+
>m2 : Maybe<null> | undefined
54+
> : ^^^^^^^^^^^^^^^^^^^^^^^
55+
56+
const test4 = m2 ?? null;
57+
>test4 : Maybe<null>
58+
> : ^^^^^^^^^^^
59+
>m2 ?? null : Maybe<null>
60+
> : ^^^^^^^^^^^
61+
>m2 : Maybe<null> | undefined
62+
> : ^^^^^^^^^^^^^^^^^^^^^^^
63+
64+
type StrOrNum = string | number
65+
>StrOrNum : StrOrNum
66+
> : ^^^^^^^^
67+
68+
declare const numOrStr: StrOrNum;
69+
>numOrStr : StrOrNum
70+
> : ^^^^^^^^
71+
72+
const test5 = numOrStr && numOrStr;
73+
>test5 : StrOrNum
74+
> : ^^^^^^^^
75+
>numOrStr && numOrStr : StrOrNum
76+
> : ^^^^^^^^
77+
>numOrStr : StrOrNum
78+
> : ^^^^^^^^
79+
>numOrStr : StrOrNum
80+
> : ^^^^^^^^
81+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// @strict: true
2+
// @noEmit: true
3+
4+
// https://github.com/microsoft/TypeScript/issues/43031
5+
6+
type Brand<K, T> = K & { __brand: T };
7+
type BrandedUnknown<T> = Brand<"unknown", T>;
8+
type Maybe<T> = T | BrandedUnknown<T>;
9+
10+
declare const m1: Maybe<boolean> | undefined;
11+
const test1 = m1 || false;
12+
const test2 = m1 ?? false;
13+
14+
declare const m2: Maybe<null> | undefined;
15+
const test3 = m2 || null;
16+
const test4 = m2 ?? null;
17+
18+
type StrOrNum = string | number
19+
declare const numOrStr: StrOrNum;
20+
const test5 = numOrStr && numOrStr;

0 commit comments

Comments
 (0)