From d25a6ec9ad10049d407407e07859ca4f09873a77 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 27 Apr 2018 10:40:16 -0700 Subject: [PATCH 1/4] Remove redundant primitive types from intersections with literal types --- src/compiler/checker.ts | 150 +++++++++++++++++++--------------------- src/compiler/types.ts | 8 +++ 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f3be433aec143..374e27f9e7766 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -617,22 +617,6 @@ namespace ts { Both = Source | Target, } - const enum TypeIncludes { - Any = 1 << 0, - Undefined = 1 << 1, - Null = 1 << 2, - Never = 1 << 3, - NonWideningType = 1 << 4, - String = 1 << 5, - Number = 1 << 6, - ESSymbol = 1 << 7, - LiteralOrUniqueESSymbol = 1 << 8, - ObjectType = 1 << 9, - EmptyObject = 1 << 10, - Union = 1 << 11, - Wildcard = 1 << 12, - } - const enum MembersOrExportsResolutionKind { resolvedExports = "resolvedExports", resolvedMembers = "resolvedMembers" @@ -7926,35 +7910,31 @@ namespace ts { return false; } - function addTypeToUnion(typeSet: Type[], includes: TypeIncludes, type: Type) { + function addTypeToUnion(typeSet: Type[], includes: TypeFlags, type: Type) { const flags = type.flags; if (flags & TypeFlags.Union) { - includes = addTypesToUnion(typeSet, includes, (type).types); - } - else if (flags & TypeFlags.Any) { - includes |= TypeIncludes.Any; - if (type === wildcardType) includes |= TypeIncludes.Wildcard; - } - else if (!strictNullChecks && flags & TypeFlags.Nullable) { - if (flags & TypeFlags.Undefined) includes |= TypeIncludes.Undefined; - if (flags & TypeFlags.Null) includes |= TypeIncludes.Null; - if (!(flags & TypeFlags.ContainsWideningType)) includes |= TypeIncludes.NonWideningType; - } - else if (!(flags & TypeFlags.Never || flags & TypeFlags.Intersection && isEmptyIntersectionType(type))) { - // We ignore 'never' types in unions. Likewise, we ignore intersections of unit types as they are - // another form of 'never' (in that they have an empty value domain). We could in theory turn - // intersections of unit types into 'never' upon construction, but deferring the reduction makes it - // easier to reason about their origin. - if (flags & TypeFlags.String) includes |= TypeIncludes.String; - if (flags & TypeFlags.Number) includes |= TypeIncludes.Number; - if (flags & TypeFlags.ESSymbol) includes |= TypeIncludes.ESSymbol; - if (flags & TypeFlags.StringOrNumberLiteralOrUnique) includes |= TypeIncludes.LiteralOrUniqueESSymbol; - const len = typeSet.length; - const index = len && type.id > typeSet[len - 1].id ? ~len : binarySearch(typeSet, type, getTypeId, compareValues); - if (index < 0) { - if (!(flags & TypeFlags.Object && (type).objectFlags & ObjectFlags.Anonymous && - type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method) && containsIdenticalType(typeSet, type))) { - typeSet.splice(~index, 0, type); + return addTypesToUnion(typeSet, includes, (type).types); + } + // We ignore 'never' types in unions. Likewise, we ignore intersections of unit types as they are + // another form of 'never' (in that they have an empty value domain). We could in theory turn + // intersections of unit types into 'never' upon construction, but deferring the reduction makes it + // easier to reason about their origin. + if (!(flags & TypeFlags.Never || flags & TypeFlags.Intersection && isEmptyIntersectionType(type))) { + includes |= flags & ~TypeFlags.ConstructionFlags; + if (flags & TypeFlags.Any) { + if (type === wildcardType) includes |= TypeFlags.Wildcard; + } + else if (!strictNullChecks && flags & TypeFlags.Nullable) { + if (!(flags & TypeFlags.ContainsWideningType)) includes |= TypeFlags.NonWideningType; + } + else { + const len = typeSet.length; + const index = len && type.id > typeSet[len - 1].id ? ~len : binarySearch(typeSet, type, getTypeId, compareValues); + if (index < 0) { + if (!(flags & TypeFlags.Object && (type).objectFlags & ObjectFlags.Anonymous && + type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method) && containsIdenticalType(typeSet, type))) { + typeSet.splice(~index, 0, type); + } } } } @@ -7963,7 +7943,7 @@ namespace ts { // 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: TypeIncludes, types: Type[]): TypeIncludes { + function addTypesToUnion(typeSet: Type[], includes: TypeFlags, types: Type[]): TypeFlags { for (const type of types) { includes = addTypeToUnion(typeSet, includes, type); } @@ -8020,15 +8000,15 @@ namespace ts { } } - function removeRedundantLiteralTypes(types: Type[], includes: TypeIncludes) { + function removeRedundantLiteralTypes(types: Type[], includes: TypeFlags) { let i = types.length; while (i > 0) { i--; const t = types[i]; const remove = - t.flags & TypeFlags.StringLiteral && includes & TypeIncludes.String || - t.flags & TypeFlags.NumberLiteral && includes & TypeIncludes.Number || - t.flags & TypeFlags.UniqueESSymbol && includes & TypeIncludes.ESSymbol || + t.flags & TypeFlags.StringLiteral && includes & TypeFlags.String || + t.flags & TypeFlags.NumberLiteral && includes & TypeFlags.Number || + t.flags & TypeFlags.UniqueESSymbol && includes & TypeFlags.ESSymbol || t.flags & TypeFlags.StringOrNumberLiteral && t.flags & TypeFlags.FreshLiteral && containsType(types, (t).regularType); if (remove) { orderedRemoveItemAt(types, i); @@ -8052,12 +8032,12 @@ namespace ts { } const typeSet: Type[] = []; const includes = addTypesToUnion(typeSet, 0, types); - if (includes & TypeIncludes.Any) { - return includes & TypeIncludes.Wildcard ? wildcardType : anyType; + if (includes & TypeFlags.Any) { + return includes & TypeFlags.Wildcard ? wildcardType : anyType; } switch (unionReduction) { case UnionReduction.Literal: - if (includes & TypeIncludes.LiteralOrUniqueESSymbol) { + if (includes & TypeFlags.StringOrNumberLiteralOrUnique) { removeRedundantLiteralTypes(typeSet, includes); } break; @@ -8066,8 +8046,8 @@ namespace ts { break; } if (typeSet.length === 0) { - return includes & TypeIncludes.Null ? includes & TypeIncludes.NonWideningType ? nullType : nullWideningType : - includes & TypeIncludes.Undefined ? includes & TypeIncludes.NonWideningType ? undefinedType : undefinedWideningType : + return includes & TypeFlags.Null ? includes & TypeFlags.NonWideningType ? nullType : nullWideningType : + includes & TypeFlags.Undefined ? includes & TypeFlags.NonWideningType ? undefinedType : undefinedWideningType : neverType; } return getUnionTypeFromSortedList(typeSet, aliasSymbol, aliasTypeArguments); @@ -8145,30 +8125,23 @@ namespace ts { return links.resolvedType; } - function addTypeToIntersection(typeSet: Type[], includes: TypeIncludes, type: Type) { + function addTypeToIntersection(typeSet: Type[], includes: TypeFlags, type: Type) { const flags = type.flags; if (flags & TypeFlags.Intersection) { - includes = addTypesToIntersection(typeSet, includes, (type).types); - } - else if (flags & TypeFlags.Any) { - includes |= TypeIncludes.Any; - if (type === wildcardType) includes |= TypeIncludes.Wildcard; - } - else if (flags & TypeFlags.Never) { - includes |= TypeIncludes.Never; + return addTypesToIntersection(typeSet, includes, (type).types); } - else if (getObjectFlags(type) & ObjectFlags.Anonymous && isEmptyObjectType(type)) { - includes |= TypeIncludes.EmptyObject; + if (getObjectFlags(type) & ObjectFlags.Anonymous && isEmptyObjectType(type)) { + includes |= TypeFlags.EmptyObject; } - else if ((strictNullChecks || !(flags & TypeFlags.Nullable)) && !contains(typeSet, type)) { - if (flags & TypeFlags.Object) { - includes |= TypeIncludes.ObjectType; - } - if (flags & TypeFlags.Union) { - includes |= TypeIncludes.Union; - } - if (!(flags & TypeFlags.Object && (type).objectFlags & ObjectFlags.Anonymous && - type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method) && containsIdenticalType(typeSet, type))) { + else { + includes |= flags & ~TypeFlags.ConstructionFlags; + if (flags & TypeFlags.Any) { + if (type === wildcardType) includes |= TypeFlags.Wildcard; + } + else if ((strictNullChecks || !(flags & TypeFlags.Nullable)) && !contains(typeSet, type) && + !(flags & TypeFlags.Object && (type).objectFlags & ObjectFlags.Anonymous && + type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method) && + containsIdenticalType(typeSet, type))) { typeSet.push(type); } } @@ -8177,13 +8150,27 @@ namespace ts { // Add the given types to the given type set. Order is preserved, freshness is removed from literal // types, duplicates are removed, and nested types of the given kind are flattened into the set. - function addTypesToIntersection(typeSet: Type[], includes: TypeIncludes, types: Type[]) { + function addTypesToIntersection(typeSet: Type[], includes: TypeFlags, types: Type[]) { for (const type of types) { includes = addTypeToIntersection(typeSet, includes, getRegularTypeOfLiteralType(type)); } return includes; } + function removeRedundantPrimtiveTypes(types: Type[], includes: TypeFlags) { + let i = types.length; + while (i > 0) { + i--; + const t = types[i]; + const remove = + t.flags & TypeFlags.String && includes & TypeFlags.StringLiteral || + t.flags & TypeFlags.Number && includes & TypeFlags.NumberLiteral || + t.flags & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol; + if (remove) { + orderedRemoveItemAt(types, i); + } + } + } // We normalize combinations of intersection and union types based on the distributive property of the '&' // operator. Specifically, because X & (A | B) is equivalent to X & A | X & B, we can transform intersection // types with union type constituents into equivalent union types with intersection type constituents and @@ -8200,19 +8187,24 @@ namespace ts { } const typeSet: Type[] = []; const includes = addTypesToIntersection(typeSet, 0, types); - if (includes & TypeIncludes.Never) { + if (includes & TypeFlags.Never) { return neverType; } - if (includes & TypeIncludes.Any) { - return includes & TypeIncludes.Wildcard ? wildcardType : anyType; + if (includes & TypeFlags.Any) { + return includes & TypeFlags.Wildcard ? wildcardType : anyType; + } + if (includes & TypeFlags.String && includes & TypeFlags.StringLiteral || + includes & TypeFlags.Number && includes & TypeFlags.NumberLiteral || + includes & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol) { + removeRedundantPrimtiveTypes(typeSet, includes); } - if (includes & TypeIncludes.EmptyObject && !(includes & TypeIncludes.ObjectType)) { + if (includes & TypeFlags.EmptyObject && !(includes & TypeFlags.Object)) { typeSet.push(emptyObjectType); } if (typeSet.length === 1) { return typeSet[0]; } - if (includes & TypeIncludes.Union) { + if (includes & TypeFlags.Union) { // We are attempting to construct a type of the form X & (A | B) & Y. Transform this into a type of // the form X & A & Y | X & B & Y and recursively reduce until no union type constituents remain. const unionIndex = findIndex(typeSet, t => (t.flags & TypeFlags.Union) !== 0); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 2aea2e2974202..c0d9b0ed9af2f 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3636,7 +3636,15 @@ namespace ts { RequiresWidening = ContainsWideningType | ContainsObjectLiteral, /* @internal */ PropagatingFlags = ContainsWideningType | ContainsObjectLiteral | ContainsAnyFunctionType, + // The following flags are used for different purposes during union and intersection type construction /* @internal */ + NonWideningType = ContainsWideningType, + /* @internal */ + Wildcard = ContainsObjectLiteral, + /* @internal */ + EmptyObject = ContainsAnyFunctionType, + /* @internal */ + ConstructionFlags = NonWideningType | Wildcard | EmptyObject } export type DestructuringPattern = BindingPattern | ObjectLiteralExpression | ArrayLiteralExpression; From 38d1f7f0d22e5d706af3e18146fb2fcb4333b4c6 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 27 Apr 2018 16:50:09 -0700 Subject: [PATCH 2/4] Add tests --- .../types/intersection/intersectionReduction.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/cases/conformance/types/intersection/intersectionReduction.ts diff --git a/tests/cases/conformance/types/intersection/intersectionReduction.ts b/tests/cases/conformance/types/intersection/intersectionReduction.ts new file mode 100644 index 0000000000000..b3b8950c33064 --- /dev/null +++ b/tests/cases/conformance/types/intersection/intersectionReduction.ts @@ -0,0 +1,15 @@ +// @strict + +declare const sym1: unique symbol; +declare const sym2: unique symbol; + +type T1 = string & 'a'; // 'a' +type T2 = 'a' & string & 'b'; // 'a' & 'b' +type T3 = number & 10; // 10 +type T4 = 10 & number & 20; // 10 & 20 +type T5 = symbol & typeof sym1; // typeof sym1 +type T6 = typeof sym1 & symbol & typeof sym2; // typeof sym1 & typeof sym2 +type T7 = string & 'a' & number & 10 & symbol & typeof sym1; // 'a' & 10 & typeof sym1 + +type T10 = string & ('a' | 'b'); // 'a' | 'b' +type T11 = (string | number) & ('a' | 10); // 'a' | 10 From e80b47da9e601f3d0ebd7173c3905f07a49e8e8c Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 27 Apr 2018 16:50:17 -0700 Subject: [PATCH 3/4] Accept new baselines --- .../reference/intersectionReduction.js | 20 ++++++++++ .../reference/intersectionReduction.symbols | 40 +++++++++++++++++++ .../reference/intersectionReduction.types | 40 +++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 tests/baselines/reference/intersectionReduction.js create mode 100644 tests/baselines/reference/intersectionReduction.symbols create mode 100644 tests/baselines/reference/intersectionReduction.types diff --git a/tests/baselines/reference/intersectionReduction.js b/tests/baselines/reference/intersectionReduction.js new file mode 100644 index 0000000000000..1b985a79b9f83 --- /dev/null +++ b/tests/baselines/reference/intersectionReduction.js @@ -0,0 +1,20 @@ +//// [intersectionReduction.ts] +// @strict + +declare const sym1: unique symbol; +declare const sym2: unique symbol; + +type T1 = string & 'a'; // 'a' +type T2 = 'a' & string & 'b'; // 'a' & 'b' +type T3 = number & 10; // 10 +type T4 = 10 & number & 20; // 10 & 20 +type T5 = symbol & typeof sym1; // typeof sym1 +type T6 = typeof sym1 & symbol & typeof sym2; // typeof sym1 & typeof sym2 +type T7 = string & 'a' & number & 10 & symbol & typeof sym1; // 'a' & 10 & typeof sym1 + +type T10 = string & ('a' | 'b'); // 'a' | 'b' +type T11 = (string | number) & ('a' | 10); // 'a' | 10 + + +//// [intersectionReduction.js] +// @strict diff --git a/tests/baselines/reference/intersectionReduction.symbols b/tests/baselines/reference/intersectionReduction.symbols new file mode 100644 index 0000000000000..607ea1d6c480d --- /dev/null +++ b/tests/baselines/reference/intersectionReduction.symbols @@ -0,0 +1,40 @@ +=== tests/cases/conformance/types/intersection/intersectionReduction.ts === +// @strict + +declare const sym1: unique symbol; +>sym1 : Symbol(sym1, Decl(intersectionReduction.ts, 2, 13)) + +declare const sym2: unique symbol; +>sym2 : Symbol(sym2, Decl(intersectionReduction.ts, 3, 13)) + +type T1 = string & 'a'; // 'a' +>T1 : Symbol(T1, Decl(intersectionReduction.ts, 3, 34)) + +type T2 = 'a' & string & 'b'; // 'a' & 'b' +>T2 : Symbol(T2, Decl(intersectionReduction.ts, 5, 23)) + +type T3 = number & 10; // 10 +>T3 : Symbol(T3, Decl(intersectionReduction.ts, 6, 29)) + +type T4 = 10 & number & 20; // 10 & 20 +>T4 : Symbol(T4, Decl(intersectionReduction.ts, 7, 22)) + +type T5 = symbol & typeof sym1; // typeof sym1 +>T5 : Symbol(T5, Decl(intersectionReduction.ts, 8, 27)) +>sym1 : Symbol(sym1, Decl(intersectionReduction.ts, 2, 13)) + +type T6 = typeof sym1 & symbol & typeof sym2; // typeof sym1 & typeof sym2 +>T6 : Symbol(T6, Decl(intersectionReduction.ts, 9, 31)) +>sym1 : Symbol(sym1, Decl(intersectionReduction.ts, 2, 13)) +>sym2 : Symbol(sym2, Decl(intersectionReduction.ts, 3, 13)) + +type T7 = string & 'a' & number & 10 & symbol & typeof sym1; // 'a' & 10 & typeof sym1 +>T7 : Symbol(T7, Decl(intersectionReduction.ts, 10, 45)) +>sym1 : Symbol(sym1, Decl(intersectionReduction.ts, 2, 13)) + +type T10 = string & ('a' | 'b'); // 'a' | 'b' +>T10 : Symbol(T10, Decl(intersectionReduction.ts, 11, 60)) + +type T11 = (string | number) & ('a' | 10); // 'a' | 10 +>T11 : Symbol(T11, Decl(intersectionReduction.ts, 13, 32)) + diff --git a/tests/baselines/reference/intersectionReduction.types b/tests/baselines/reference/intersectionReduction.types new file mode 100644 index 0000000000000..f8c178cfca43f --- /dev/null +++ b/tests/baselines/reference/intersectionReduction.types @@ -0,0 +1,40 @@ +=== tests/cases/conformance/types/intersection/intersectionReduction.ts === +// @strict + +declare const sym1: unique symbol; +>sym1 : unique symbol + +declare const sym2: unique symbol; +>sym2 : unique symbol + +type T1 = string & 'a'; // 'a' +>T1 : "a" + +type T2 = 'a' & string & 'b'; // 'a' & 'b' +>T2 : T2 + +type T3 = number & 10; // 10 +>T3 : 10 + +type T4 = 10 & number & 20; // 10 & 20 +>T4 : T4 + +type T5 = symbol & typeof sym1; // typeof sym1 +>T5 : unique symbol +>sym1 : unique symbol + +type T6 = typeof sym1 & symbol & typeof sym2; // typeof sym1 & typeof sym2 +>T6 : T6 +>sym1 : unique symbol +>sym2 : unique symbol + +type T7 = string & 'a' & number & 10 & symbol & typeof sym1; // 'a' & 10 & typeof sym1 +>T7 : T7 +>sym1 : unique symbol + +type T10 = string & ('a' | 'b'); // 'a' | 'b' +>T10 : "a" | "b" + +type T11 = (string | number) & ('a' | 10); // 'a' | 10 +>T11 : "a" | 10 + From 5a7eb1cbc89b8d850b4bc57a4dd72ca1109cc03b Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sat, 28 Apr 2018 08:24:41 -0700 Subject: [PATCH 4/4] Fix typo --- src/compiler/checker.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f0cbd70ed9ce0..22c8add26de08 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8250,7 +8250,7 @@ namespace ts { return includes; } - function removeRedundantPrimtiveTypes(types: Type[], includes: TypeFlags) { + function removeRedundantPrimitiveTypes(types: Type[], includes: TypeFlags) { let i = types.length; while (i > 0) { i--; @@ -8264,6 +8264,7 @@ namespace ts { } } } + // We normalize combinations of intersection and union types based on the distributive property of the '&' // operator. Specifically, because X & (A | B) is equivalent to X & A | X & B, we can transform intersection // types with union type constituents into equivalent union types with intersection type constituents and @@ -8289,7 +8290,7 @@ namespace ts { if (includes & TypeFlags.String && includes & TypeFlags.StringLiteral || includes & TypeFlags.Number && includes & TypeFlags.NumberLiteral || includes & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol) { - removeRedundantPrimtiveTypes(typeSet, includes); + removeRedundantPrimitiveTypes(typeSet, includes); } if (includes & TypeFlags.EmptyObject && !(includes & TypeFlags.Object)) { typeSet.push(emptyObjectType);