Skip to content

Commit 25d5952

Browse files
authored
Merge pull request #23751 from Microsoft/reduceIntersectionTypes
Remove redundant primitive types from intersections
2 parents 52b809f + 5a7eb1c commit 25d5952

File tree

6 files changed

+195
-79
lines changed

6 files changed

+195
-79
lines changed

src/compiler/checker.ts

+72-79
Original file line numberDiff line numberDiff line change
@@ -618,22 +618,6 @@ namespace ts {
618618
Both = Source | Target,
619619
}
620620

621-
const enum TypeIncludes {
622-
Any = 1 << 0,
623-
Undefined = 1 << 1,
624-
Null = 1 << 2,
625-
Never = 1 << 3,
626-
NonWideningType = 1 << 4,
627-
String = 1 << 5,
628-
Number = 1 << 6,
629-
ESSymbol = 1 << 7,
630-
LiteralOrUniqueESSymbol = 1 << 8,
631-
ObjectType = 1 << 9,
632-
EmptyObject = 1 << 10,
633-
Union = 1 << 11,
634-
Wildcard = 1 << 12,
635-
}
636-
637621
const enum MembersOrExportsResolutionKind {
638622
resolvedExports = "resolvedExports",
639623
resolvedMembers = "resolvedMembers"
@@ -8047,35 +8031,31 @@ namespace ts {
80478031
return false;
80488032
}
80498033

8050-
function addTypeToUnion(typeSet: Type[], includes: TypeIncludes, type: Type) {
8034+
function addTypeToUnion(typeSet: Type[], includes: TypeFlags, type: Type) {
80518035
const flags = type.flags;
80528036
if (flags & TypeFlags.Union) {
8053-
includes = addTypesToUnion(typeSet, includes, (<UnionType>type).types);
8054-
}
8055-
else if (flags & TypeFlags.Any) {
8056-
includes |= TypeIncludes.Any;
8057-
if (type === wildcardType) includes |= TypeIncludes.Wildcard;
8058-
}
8059-
else if (!strictNullChecks && flags & TypeFlags.Nullable) {
8060-
if (flags & TypeFlags.Undefined) includes |= TypeIncludes.Undefined;
8061-
if (flags & TypeFlags.Null) includes |= TypeIncludes.Null;
8062-
if (!(flags & TypeFlags.ContainsWideningType)) includes |= TypeIncludes.NonWideningType;
8063-
}
8064-
else if (!(flags & TypeFlags.Never || flags & TypeFlags.Intersection && isEmptyIntersectionType(<IntersectionType>type))) {
8065-
// We ignore 'never' types in unions. Likewise, we ignore intersections of unit types as they are
8066-
// another form of 'never' (in that they have an empty value domain). We could in theory turn
8067-
// intersections of unit types into 'never' upon construction, but deferring the reduction makes it
8068-
// easier to reason about their origin.
8069-
if (flags & TypeFlags.String) includes |= TypeIncludes.String;
8070-
if (flags & TypeFlags.Number) includes |= TypeIncludes.Number;
8071-
if (flags & TypeFlags.ESSymbol) includes |= TypeIncludes.ESSymbol;
8072-
if (flags & TypeFlags.StringOrNumberLiteralOrUnique) includes |= TypeIncludes.LiteralOrUniqueESSymbol;
8073-
const len = typeSet.length;
8074-
const index = len && type.id > typeSet[len - 1].id ? ~len : binarySearch(typeSet, type, getTypeId, compareValues);
8075-
if (index < 0) {
8076-
if (!(flags & TypeFlags.Object && (<ObjectType>type).objectFlags & ObjectFlags.Anonymous &&
8077-
type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method) && containsIdenticalType(typeSet, type))) {
8078-
typeSet.splice(~index, 0, type);
8037+
return addTypesToUnion(typeSet, includes, (<UnionType>type).types);
8038+
}
8039+
// We ignore 'never' types in unions. Likewise, we ignore intersections of unit types as they are
8040+
// another form of 'never' (in that they have an empty value domain). We could in theory turn
8041+
// intersections of unit types into 'never' upon construction, but deferring the reduction makes it
8042+
// easier to reason about their origin.
8043+
if (!(flags & TypeFlags.Never || flags & TypeFlags.Intersection && isEmptyIntersectionType(<IntersectionType>type))) {
8044+
includes |= flags & ~TypeFlags.ConstructionFlags;
8045+
if (flags & TypeFlags.Any) {
8046+
if (type === wildcardType) includes |= TypeFlags.Wildcard;
8047+
}
8048+
else if (!strictNullChecks && flags & TypeFlags.Nullable) {
8049+
if (!(flags & TypeFlags.ContainsWideningType)) includes |= TypeFlags.NonWideningType;
8050+
}
8051+
else {
8052+
const len = typeSet.length;
8053+
const index = len && type.id > typeSet[len - 1].id ? ~len : binarySearch(typeSet, type, getTypeId, compareValues);
8054+
if (index < 0) {
8055+
if (!(flags & TypeFlags.Object && (<ObjectType>type).objectFlags & ObjectFlags.Anonymous &&
8056+
type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method) && containsIdenticalType(typeSet, type))) {
8057+
typeSet.splice(~index, 0, type);
8058+
}
80798059
}
80808060
}
80818061
}
@@ -8084,7 +8064,7 @@ namespace ts {
80848064

80858065
// Add the given types to the given type set. Order is preserved, duplicates are removed,
80868066
// and nested types of the given kind are flattened into the set.
8087-
function addTypesToUnion(typeSet: Type[], includes: TypeIncludes, types: Type[]): TypeIncludes {
8067+
function addTypesToUnion(typeSet: Type[], includes: TypeFlags, types: Type[]): TypeFlags {
80888068
for (const type of types) {
80898069
includes = addTypeToUnion(typeSet, includes, type);
80908070
}
@@ -8141,15 +8121,15 @@ namespace ts {
81418121
}
81428122
}
81438123

8144-
function removeRedundantLiteralTypes(types: Type[], includes: TypeIncludes) {
8124+
function removeRedundantLiteralTypes(types: Type[], includes: TypeFlags) {
81458125
let i = types.length;
81468126
while (i > 0) {
81478127
i--;
81488128
const t = types[i];
81498129
const remove =
8150-
t.flags & TypeFlags.StringLiteral && includes & TypeIncludes.String ||
8151-
t.flags & TypeFlags.NumberLiteral && includes & TypeIncludes.Number ||
8152-
t.flags & TypeFlags.UniqueESSymbol && includes & TypeIncludes.ESSymbol ||
8130+
t.flags & TypeFlags.StringLiteral && includes & TypeFlags.String ||
8131+
t.flags & TypeFlags.NumberLiteral && includes & TypeFlags.Number ||
8132+
t.flags & TypeFlags.UniqueESSymbol && includes & TypeFlags.ESSymbol ||
81538133
t.flags & TypeFlags.StringOrNumberLiteral && t.flags & TypeFlags.FreshLiteral && containsType(types, (<LiteralType>t).regularType);
81548134
if (remove) {
81558135
orderedRemoveItemAt(types, i);
@@ -8173,12 +8153,12 @@ namespace ts {
81738153
}
81748154
const typeSet: Type[] = [];
81758155
const includes = addTypesToUnion(typeSet, 0, types);
8176-
if (includes & TypeIncludes.Any) {
8177-
return includes & TypeIncludes.Wildcard ? wildcardType : anyType;
8156+
if (includes & TypeFlags.Any) {
8157+
return includes & TypeFlags.Wildcard ? wildcardType : anyType;
81788158
}
81798159
switch (unionReduction) {
81808160
case UnionReduction.Literal:
8181-
if (includes & TypeIncludes.LiteralOrUniqueESSymbol) {
8161+
if (includes & TypeFlags.StringOrNumberLiteralOrUnique) {
81828162
removeRedundantLiteralTypes(typeSet, includes);
81838163
}
81848164
break;
@@ -8187,8 +8167,8 @@ namespace ts {
81878167
break;
81888168
}
81898169
if (typeSet.length === 0) {
8190-
return includes & TypeIncludes.Null ? includes & TypeIncludes.NonWideningType ? nullType : nullWideningType :
8191-
includes & TypeIncludes.Undefined ? includes & TypeIncludes.NonWideningType ? undefinedType : undefinedWideningType :
8170+
return includes & TypeFlags.Null ? includes & TypeFlags.NonWideningType ? nullType : nullWideningType :
8171+
includes & TypeFlags.Undefined ? includes & TypeFlags.NonWideningType ? undefinedType : undefinedWideningType :
81928172
neverType;
81938173
}
81948174
return getUnionTypeFromSortedList(typeSet, aliasSymbol, aliasTypeArguments);
@@ -8266,30 +8246,23 @@ namespace ts {
82668246
return links.resolvedType;
82678247
}
82688248

8269-
function addTypeToIntersection(typeSet: Type[], includes: TypeIncludes, type: Type) {
8249+
function addTypeToIntersection(typeSet: Type[], includes: TypeFlags, type: Type) {
82708250
const flags = type.flags;
82718251
if (flags & TypeFlags.Intersection) {
8272-
includes = addTypesToIntersection(typeSet, includes, (<IntersectionType>type).types);
8273-
}
8274-
else if (flags & TypeFlags.Any) {
8275-
includes |= TypeIncludes.Any;
8276-
if (type === wildcardType) includes |= TypeIncludes.Wildcard;
8277-
}
8278-
else if (flags & TypeFlags.Never) {
8279-
includes |= TypeIncludes.Never;
8252+
return addTypesToIntersection(typeSet, includes, (<IntersectionType>type).types);
82808253
}
8281-
else if (getObjectFlags(type) & ObjectFlags.Anonymous && isEmptyObjectType(type)) {
8282-
includes |= TypeIncludes.EmptyObject;
8254+
if (getObjectFlags(type) & ObjectFlags.Anonymous && isEmptyObjectType(type)) {
8255+
includes |= TypeFlags.EmptyObject;
82838256
}
8284-
else if ((strictNullChecks || !(flags & TypeFlags.Nullable)) && !contains(typeSet, type)) {
8285-
if (flags & TypeFlags.Object) {
8286-
includes |= TypeIncludes.ObjectType;
8287-
}
8288-
if (flags & TypeFlags.Union) {
8289-
includes |= TypeIncludes.Union;
8290-
}
8291-
if (!(flags & TypeFlags.Object && (<ObjectType>type).objectFlags & ObjectFlags.Anonymous &&
8292-
type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method) && containsIdenticalType(typeSet, type))) {
8257+
else {
8258+
includes |= flags & ~TypeFlags.ConstructionFlags;
8259+
if (flags & TypeFlags.Any) {
8260+
if (type === wildcardType) includes |= TypeFlags.Wildcard;
8261+
}
8262+
else if ((strictNullChecks || !(flags & TypeFlags.Nullable)) && !contains(typeSet, type) &&
8263+
!(flags & TypeFlags.Object && (<ObjectType>type).objectFlags & ObjectFlags.Anonymous &&
8264+
type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method) &&
8265+
containsIdenticalType(typeSet, type))) {
82938266
typeSet.push(type);
82948267
}
82958268
}
@@ -8298,13 +8271,28 @@ namespace ts {
82988271

82998272
// Add the given types to the given type set. Order is preserved, freshness is removed from literal
83008273
// types, duplicates are removed, and nested types of the given kind are flattened into the set.
8301-
function addTypesToIntersection(typeSet: Type[], includes: TypeIncludes, types: Type[]) {
8274+
function addTypesToIntersection(typeSet: Type[], includes: TypeFlags, types: Type[]) {
83028275
for (const type of types) {
83038276
includes = addTypeToIntersection(typeSet, includes, getRegularTypeOfLiteralType(type));
83048277
}
83058278
return includes;
83068279
}
83078280

8281+
function removeRedundantPrimitiveTypes(types: Type[], includes: TypeFlags) {
8282+
let i = types.length;
8283+
while (i > 0) {
8284+
i--;
8285+
const t = types[i];
8286+
const remove =
8287+
t.flags & TypeFlags.String && includes & TypeFlags.StringLiteral ||
8288+
t.flags & TypeFlags.Number && includes & TypeFlags.NumberLiteral ||
8289+
t.flags & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol;
8290+
if (remove) {
8291+
orderedRemoveItemAt(types, i);
8292+
}
8293+
}
8294+
}
8295+
83088296
// We normalize combinations of intersection and union types based on the distributive property of the '&'
83098297
// operator. Specifically, because X & (A | B) is equivalent to X & A | X & B, we can transform intersection
83108298
// types with union type constituents into equivalent union types with intersection type constituents and
@@ -8321,19 +8309,24 @@ namespace ts {
83218309
}
83228310
const typeSet: Type[] = [];
83238311
const includes = addTypesToIntersection(typeSet, 0, types);
8324-
if (includes & TypeIncludes.Never) {
8312+
if (includes & TypeFlags.Never) {
83258313
return neverType;
83268314
}
8327-
if (includes & TypeIncludes.Any) {
8328-
return includes & TypeIncludes.Wildcard ? wildcardType : anyType;
8315+
if (includes & TypeFlags.Any) {
8316+
return includes & TypeFlags.Wildcard ? wildcardType : anyType;
8317+
}
8318+
if (includes & TypeFlags.String && includes & TypeFlags.StringLiteral ||
8319+
includes & TypeFlags.Number && includes & TypeFlags.NumberLiteral ||
8320+
includes & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol) {
8321+
removeRedundantPrimitiveTypes(typeSet, includes);
83298322
}
8330-
if (includes & TypeIncludes.EmptyObject && !(includes & TypeIncludes.ObjectType)) {
8323+
if (includes & TypeFlags.EmptyObject && !(includes & TypeFlags.Object)) {
83318324
typeSet.push(emptyObjectType);
83328325
}
83338326
if (typeSet.length === 1) {
83348327
return typeSet[0];
83358328
}
8336-
if (includes & TypeIncludes.Union) {
8329+
if (includes & TypeFlags.Union) {
83378330
// We are attempting to construct a type of the form X & (A | B) & Y. Transform this into a type of
83388331
// the form X & A & Y | X & B & Y and recursively reduce until no union type constituents remain.
83398332
const unionIndex = findIndex(typeSet, t => (t.flags & TypeFlags.Union) !== 0);

src/compiler/types.ts

+8
Original file line numberDiff line numberDiff line change
@@ -3650,7 +3650,15 @@ namespace ts {
36503650
RequiresWidening = ContainsWideningType | ContainsObjectLiteral,
36513651
/* @internal */
36523652
PropagatingFlags = ContainsWideningType | ContainsObjectLiteral | ContainsAnyFunctionType,
3653+
// The following flags are used for different purposes during union and intersection type construction
36533654
/* @internal */
3655+
NonWideningType = ContainsWideningType,
3656+
/* @internal */
3657+
Wildcard = ContainsObjectLiteral,
3658+
/* @internal */
3659+
EmptyObject = ContainsAnyFunctionType,
3660+
/* @internal */
3661+
ConstructionFlags = NonWideningType | Wildcard | EmptyObject
36543662
}
36553663

36563664
export type DestructuringPattern = BindingPattern | ObjectLiteralExpression | ArrayLiteralExpression;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//// [intersectionReduction.ts]
2+
// @strict
3+
4+
declare const sym1: unique symbol;
5+
declare const sym2: unique symbol;
6+
7+
type T1 = string & 'a'; // 'a'
8+
type T2 = 'a' & string & 'b'; // 'a' & 'b'
9+
type T3 = number & 10; // 10
10+
type T4 = 10 & number & 20; // 10 & 20
11+
type T5 = symbol & typeof sym1; // typeof sym1
12+
type T6 = typeof sym1 & symbol & typeof sym2; // typeof sym1 & typeof sym2
13+
type T7 = string & 'a' & number & 10 & symbol & typeof sym1; // 'a' & 10 & typeof sym1
14+
15+
type T10 = string & ('a' | 'b'); // 'a' | 'b'
16+
type T11 = (string | number) & ('a' | 10); // 'a' | 10
17+
18+
19+
//// [intersectionReduction.js]
20+
// @strict
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
=== tests/cases/conformance/types/intersection/intersectionReduction.ts ===
2+
// @strict
3+
4+
declare const sym1: unique symbol;
5+
>sym1 : Symbol(sym1, Decl(intersectionReduction.ts, 2, 13))
6+
7+
declare const sym2: unique symbol;
8+
>sym2 : Symbol(sym2, Decl(intersectionReduction.ts, 3, 13))
9+
10+
type T1 = string & 'a'; // 'a'
11+
>T1 : Symbol(T1, Decl(intersectionReduction.ts, 3, 34))
12+
13+
type T2 = 'a' & string & 'b'; // 'a' & 'b'
14+
>T2 : Symbol(T2, Decl(intersectionReduction.ts, 5, 23))
15+
16+
type T3 = number & 10; // 10
17+
>T3 : Symbol(T3, Decl(intersectionReduction.ts, 6, 29))
18+
19+
type T4 = 10 & number & 20; // 10 & 20
20+
>T4 : Symbol(T4, Decl(intersectionReduction.ts, 7, 22))
21+
22+
type T5 = symbol & typeof sym1; // typeof sym1
23+
>T5 : Symbol(T5, Decl(intersectionReduction.ts, 8, 27))
24+
>sym1 : Symbol(sym1, Decl(intersectionReduction.ts, 2, 13))
25+
26+
type T6 = typeof sym1 & symbol & typeof sym2; // typeof sym1 & typeof sym2
27+
>T6 : Symbol(T6, Decl(intersectionReduction.ts, 9, 31))
28+
>sym1 : Symbol(sym1, Decl(intersectionReduction.ts, 2, 13))
29+
>sym2 : Symbol(sym2, Decl(intersectionReduction.ts, 3, 13))
30+
31+
type T7 = string & 'a' & number & 10 & symbol & typeof sym1; // 'a' & 10 & typeof sym1
32+
>T7 : Symbol(T7, Decl(intersectionReduction.ts, 10, 45))
33+
>sym1 : Symbol(sym1, Decl(intersectionReduction.ts, 2, 13))
34+
35+
type T10 = string & ('a' | 'b'); // 'a' | 'b'
36+
>T10 : Symbol(T10, Decl(intersectionReduction.ts, 11, 60))
37+
38+
type T11 = (string | number) & ('a' | 10); // 'a' | 10
39+
>T11 : Symbol(T11, Decl(intersectionReduction.ts, 13, 32))
40+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
=== tests/cases/conformance/types/intersection/intersectionReduction.ts ===
2+
// @strict
3+
4+
declare const sym1: unique symbol;
5+
>sym1 : unique symbol
6+
7+
declare const sym2: unique symbol;
8+
>sym2 : unique symbol
9+
10+
type T1 = string & 'a'; // 'a'
11+
>T1 : "a"
12+
13+
type T2 = 'a' & string & 'b'; // 'a' & 'b'
14+
>T2 : T2
15+
16+
type T3 = number & 10; // 10
17+
>T3 : 10
18+
19+
type T4 = 10 & number & 20; // 10 & 20
20+
>T4 : T4
21+
22+
type T5 = symbol & typeof sym1; // typeof sym1
23+
>T5 : unique symbol
24+
>sym1 : unique symbol
25+
26+
type T6 = typeof sym1 & symbol & typeof sym2; // typeof sym1 & typeof sym2
27+
>T6 : T6
28+
>sym1 : unique symbol
29+
>sym2 : unique symbol
30+
31+
type T7 = string & 'a' & number & 10 & symbol & typeof sym1; // 'a' & 10 & typeof sym1
32+
>T7 : T7
33+
>sym1 : unique symbol
34+
35+
type T10 = string & ('a' | 'b'); // 'a' | 'b'
36+
>T10 : "a" | "b"
37+
38+
type T11 = (string | number) & ('a' | 10); // 'a' | 10
39+
>T11 : "a" | 10
40+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// @strict
2+
3+
declare const sym1: unique symbol;
4+
declare const sym2: unique symbol;
5+
6+
type T1 = string & 'a'; // 'a'
7+
type T2 = 'a' & string & 'b'; // 'a' & 'b'
8+
type T3 = number & 10; // 10
9+
type T4 = 10 & number & 20; // 10 & 20
10+
type T5 = symbol & typeof sym1; // typeof sym1
11+
type T6 = typeof sym1 & symbol & typeof sym2; // typeof sym1 & typeof sym2
12+
type T7 = string & 'a' & number & 10 & symbol & typeof sym1; // 'a' & 10 & typeof sym1
13+
14+
type T10 = string & ('a' | 'b'); // 'a' | 'b'
15+
type T11 = (string | number) & ('a' | 10); // 'a' | 10

0 commit comments

Comments
 (0)