From ed7a7b81216a13143ba7d2044a1712a7de30f56c Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 5 Apr 2021 11:07:45 -1000 Subject: [PATCH 1/5] Ignore object types in intersections with primitive types --- src/compiler/checker.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 6bb2aa7fe294d..0aa120167f181 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22013,7 +22013,10 @@ namespace ts { return reduceLeft((type).types, (facts, t) => facts | getTypeFacts(t), TypeFacts.None); } if (flags & TypeFlags.Intersection) { - return reduceLeft((type).types, (facts, t) => facts & getTypeFacts(t), TypeFacts.All); + // When an intersection contains a primitive type we ignore object type constituents as they are + // presumably type tags. For example, in string & { __kind__: "name" } we ignore the object type. + const containsPrimitive = maybeTypeOfKind(type, TypeFlags.Primitive); + return reduceLeft((type).types, (facts, t) => t.flags & TypeFlags.Object && containsPrimitive ? facts : facts & getTypeFacts(t), TypeFacts.All); } return TypeFacts.All; } From 6e3d75f9543b3a9f802d0f29df755abbfcb2ff65 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 5 Apr 2021 11:15:19 -1000 Subject: [PATCH 2/5] Add regression test --- .../reference/taggedPrimitiveNarrowing.js | 26 ++++++++++++++++++ .../taggedPrimitiveNarrowing.symbols | 22 +++++++++++++++ .../reference/taggedPrimitiveNarrowing.types | 27 +++++++++++++++++++ .../compiler/taggedPrimitiveNarrowing.ts | 11 ++++++++ 4 files changed, 86 insertions(+) create mode 100644 tests/baselines/reference/taggedPrimitiveNarrowing.js create mode 100644 tests/baselines/reference/taggedPrimitiveNarrowing.symbols create mode 100644 tests/baselines/reference/taggedPrimitiveNarrowing.types create mode 100644 tests/cases/compiler/taggedPrimitiveNarrowing.ts diff --git a/tests/baselines/reference/taggedPrimitiveNarrowing.js b/tests/baselines/reference/taggedPrimitiveNarrowing.js new file mode 100644 index 0000000000000..45ba67f8a7852 --- /dev/null +++ b/tests/baselines/reference/taggedPrimitiveNarrowing.js @@ -0,0 +1,26 @@ +//// [taggedPrimitiveNarrowing.ts] +type Hash = string & { __hash: true }; + +function getHashLength(hash: Hash): number { + if (typeof hash !== "string") { + throw new Error("This doesn't look like a hash"); + } + return hash.length; +} + + +//// [taggedPrimitiveNarrowing.js] +"use strict"; +function getHashLength(hash) { + if (typeof hash !== "string") { + throw new Error("This doesn't look like a hash"); + } + return hash.length; +} + + +//// [taggedPrimitiveNarrowing.d.ts] +declare type Hash = string & { + __hash: true; +}; +declare function getHashLength(hash: Hash): number; diff --git a/tests/baselines/reference/taggedPrimitiveNarrowing.symbols b/tests/baselines/reference/taggedPrimitiveNarrowing.symbols new file mode 100644 index 0000000000000..f19a30de10834 --- /dev/null +++ b/tests/baselines/reference/taggedPrimitiveNarrowing.symbols @@ -0,0 +1,22 @@ +=== tests/cases/compiler/taggedPrimitiveNarrowing.ts === +type Hash = string & { __hash: true }; +>Hash : Symbol(Hash, Decl(taggedPrimitiveNarrowing.ts, 0, 0)) +>__hash : Symbol(__hash, Decl(taggedPrimitiveNarrowing.ts, 0, 22)) + +function getHashLength(hash: Hash): number { +>getHashLength : Symbol(getHashLength, Decl(taggedPrimitiveNarrowing.ts, 0, 38)) +>hash : Symbol(hash, Decl(taggedPrimitiveNarrowing.ts, 2, 23)) +>Hash : Symbol(Hash, Decl(taggedPrimitiveNarrowing.ts, 0, 0)) + + if (typeof hash !== "string") { +>hash : Symbol(hash, Decl(taggedPrimitiveNarrowing.ts, 2, 23)) + + throw new Error("This doesn't look like a hash"); +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + } + return hash.length; +>hash.length : Symbol(String.length, Decl(lib.es5.d.ts, --, --)) +>hash : Symbol(hash, Decl(taggedPrimitiveNarrowing.ts, 2, 23)) +>length : Symbol(String.length, Decl(lib.es5.d.ts, --, --)) +} + diff --git a/tests/baselines/reference/taggedPrimitiveNarrowing.types b/tests/baselines/reference/taggedPrimitiveNarrowing.types new file mode 100644 index 0000000000000..4e63f915ebd41 --- /dev/null +++ b/tests/baselines/reference/taggedPrimitiveNarrowing.types @@ -0,0 +1,27 @@ +=== tests/cases/compiler/taggedPrimitiveNarrowing.ts === +type Hash = string & { __hash: true }; +>Hash : Hash +>__hash : true +>true : true + +function getHashLength(hash: Hash): number { +>getHashLength : (hash: Hash) => number +>hash : Hash + + if (typeof hash !== "string") { +>typeof hash !== "string" : boolean +>typeof hash : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>hash : Hash +>"string" : "string" + + throw new Error("This doesn't look like a hash"); +>new Error("This doesn't look like a hash") : Error +>Error : ErrorConstructor +>"This doesn't look like a hash" : "This doesn't look like a hash" + } + return hash.length; +>hash.length : number +>hash : Hash +>length : number +} + diff --git a/tests/cases/compiler/taggedPrimitiveNarrowing.ts b/tests/cases/compiler/taggedPrimitiveNarrowing.ts new file mode 100644 index 0000000000000..9c32291ab56f0 --- /dev/null +++ b/tests/cases/compiler/taggedPrimitiveNarrowing.ts @@ -0,0 +1,11 @@ +// @strict: true +// @declaration: true + +type Hash = string & { __hash: true }; + +function getHashLength(hash: Hash): number { + if (typeof hash !== "string") { + throw new Error("This doesn't look like a hash"); + } + return hash.length; +} From a6bc86ecac71dc240d74ec1ff1c1f423bf8b84d1 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Tue, 6 Apr 2021 09:01:13 -1000 Subject: [PATCH 3/5] Also handle instantiable types constrained to object types --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0aa120167f181..be4e62637904b 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22016,7 +22016,7 @@ namespace ts { // When an intersection contains a primitive type we ignore object type constituents as they are // presumably type tags. For example, in string & { __kind__: "name" } we ignore the object type. const containsPrimitive = maybeTypeOfKind(type, TypeFlags.Primitive); - return reduceLeft((type).types, (facts, t) => t.flags & TypeFlags.Object && containsPrimitive ? facts : facts & getTypeFacts(t), TypeFacts.All); + return reduceLeft((type).types, (facts, t) => containsPrimitive && getBaseConstraintOrType(t).flags & TypeFlags.Object ? facts : facts & getTypeFacts(t), TypeFacts.All); } return TypeFacts.All; } From d4735eaa70dc822ba3886532a69c9e3da1d7777d Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Tue, 6 Apr 2021 09:11:18 -1000 Subject: [PATCH 4/5] Add another test --- .../reference/taggedPrimitiveNarrowing.js | 16 ++++++++++++++ .../taggedPrimitiveNarrowing.symbols | 19 ++++++++++++++++ .../reference/taggedPrimitiveNarrowing.types | 22 +++++++++++++++++++ .../compiler/taggedPrimitiveNarrowing.ts | 7 ++++++ 4 files changed, 64 insertions(+) diff --git a/tests/baselines/reference/taggedPrimitiveNarrowing.js b/tests/baselines/reference/taggedPrimitiveNarrowing.js index 45ba67f8a7852..a87ea61ddf32c 100644 --- a/tests/baselines/reference/taggedPrimitiveNarrowing.js +++ b/tests/baselines/reference/taggedPrimitiveNarrowing.js @@ -7,6 +7,13 @@ function getHashLength(hash: Hash): number { } return hash.length; } + +function getHashLength2(hash: string & T): number { + if (typeof hash !== "string") { + throw new Error("This doesn't look like a hash"); + } + return hash.length; +} //// [taggedPrimitiveNarrowing.js] @@ -17,6 +24,12 @@ function getHashLength(hash) { } return hash.length; } +function getHashLength2(hash) { + if (typeof hash !== "string") { + throw new Error("This doesn't look like a hash"); + } + return hash.length; +} //// [taggedPrimitiveNarrowing.d.ts] @@ -24,3 +37,6 @@ declare type Hash = string & { __hash: true; }; declare function getHashLength(hash: Hash): number; +declare function getHashLength2(hash: string & T): number; diff --git a/tests/baselines/reference/taggedPrimitiveNarrowing.symbols b/tests/baselines/reference/taggedPrimitiveNarrowing.symbols index f19a30de10834..50fd9bdb40caf 100644 --- a/tests/baselines/reference/taggedPrimitiveNarrowing.symbols +++ b/tests/baselines/reference/taggedPrimitiveNarrowing.symbols @@ -20,3 +20,22 @@ function getHashLength(hash: Hash): number { >length : Symbol(String.length, Decl(lib.es5.d.ts, --, --)) } +function getHashLength2(hash: string & T): number { +>getHashLength2 : Symbol(getHashLength2, Decl(taggedPrimitiveNarrowing.ts, 7, 1)) +>T : Symbol(T, Decl(taggedPrimitiveNarrowing.ts, 9, 24)) +>__tag__ : Symbol(__tag__, Decl(taggedPrimitiveNarrowing.ts, 9, 35)) +>hash : Symbol(hash, Decl(taggedPrimitiveNarrowing.ts, 9, 55)) +>T : Symbol(T, Decl(taggedPrimitiveNarrowing.ts, 9, 24)) + + if (typeof hash !== "string") { +>hash : Symbol(hash, Decl(taggedPrimitiveNarrowing.ts, 9, 55)) + + throw new Error("This doesn't look like a hash"); +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + } + return hash.length; +>hash.length : Symbol(String.length, Decl(lib.es5.d.ts, --, --)) +>hash : Symbol(hash, Decl(taggedPrimitiveNarrowing.ts, 9, 55)) +>length : Symbol(String.length, Decl(lib.es5.d.ts, --, --)) +} + diff --git a/tests/baselines/reference/taggedPrimitiveNarrowing.types b/tests/baselines/reference/taggedPrimitiveNarrowing.types index 4e63f915ebd41..c26e4174b593d 100644 --- a/tests/baselines/reference/taggedPrimitiveNarrowing.types +++ b/tests/baselines/reference/taggedPrimitiveNarrowing.types @@ -25,3 +25,25 @@ function getHashLength(hash: Hash): number { >length : number } +function getHashLength2(hash: string & T): number { +>getHashLength2 : (hash: string & T) => number +>__tag__ : unknown +>hash : string & T + + if (typeof hash !== "string") { +>typeof hash !== "string" : boolean +>typeof hash : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>hash : string & T +>"string" : "string" + + throw new Error("This doesn't look like a hash"); +>new Error("This doesn't look like a hash") : Error +>Error : ErrorConstructor +>"This doesn't look like a hash" : "This doesn't look like a hash" + } + return hash.length; +>hash.length : number +>hash : string & T +>length : number +} + diff --git a/tests/cases/compiler/taggedPrimitiveNarrowing.ts b/tests/cases/compiler/taggedPrimitiveNarrowing.ts index 9c32291ab56f0..50ca3704abe89 100644 --- a/tests/cases/compiler/taggedPrimitiveNarrowing.ts +++ b/tests/cases/compiler/taggedPrimitiveNarrowing.ts @@ -9,3 +9,10 @@ function getHashLength(hash: Hash): number { } return hash.length; } + +function getHashLength2(hash: string & T): number { + if (typeof hash !== "string") { + throw new Error("This doesn't look like a hash"); + } + return hash.length; +} From a7f8a8222a8c0f4da88122072bb5a946eac8e185 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Wed, 7 Apr 2021 10:10:58 -1000 Subject: [PATCH 5/5] Add ignoreObjects optional parameter to getTypeFacts --- src/compiler/checker.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index be4e62637904b..cdbd02b9b8d7d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -21946,7 +21946,7 @@ namespace ts { resolved.members.get("bind" as __String) && isTypeSubtypeOf(type, globalFunctionType)); } - function getTypeFacts(type: Type): TypeFacts { + function getTypeFacts(type: Type, ignoreObjects = false): TypeFacts { const flags = type.flags; if (flags & TypeFlags.String) { return strictNullChecks ? TypeFacts.StringStrictFacts : TypeFacts.StringFacts; @@ -21983,7 +21983,7 @@ namespace ts { (type === falseType || type === regularFalseType) ? TypeFacts.FalseStrictFacts : TypeFacts.TrueStrictFacts : (type === falseType || type === regularFalseType) ? TypeFacts.FalseFacts : TypeFacts.TrueFacts; } - if (flags & TypeFlags.Object) { + if (flags & TypeFlags.Object && !ignoreObjects) { return getObjectFlags(type) & ObjectFlags.Anonymous && isEmptyObjectType(type) ? strictNullChecks ? TypeFacts.EmptyObjectStrictFacts : TypeFacts.EmptyObjectFacts : isFunctionObjectType(type) ? @@ -22006,17 +22006,17 @@ namespace ts { return TypeFacts.None; } if (flags & TypeFlags.Instantiable) { - return !isPatternLiteralType(type) ? getTypeFacts(getBaseConstraintOfType(type) || unknownType) : + return !isPatternLiteralType(type) ? getTypeFacts(getBaseConstraintOfType(type) || unknownType, ignoreObjects) : strictNullChecks ? TypeFacts.NonEmptyStringStrictFacts : TypeFacts.NonEmptyStringFacts; } if (flags & TypeFlags.Union) { - return reduceLeft((type).types, (facts, t) => facts | getTypeFacts(t), TypeFacts.None); + return reduceLeft((type).types, (facts, t) => facts | getTypeFacts(t, ignoreObjects), TypeFacts.None); } if (flags & TypeFlags.Intersection) { // When an intersection contains a primitive type we ignore object type constituents as they are // presumably type tags. For example, in string & { __kind__: "name" } we ignore the object type. - const containsPrimitive = maybeTypeOfKind(type, TypeFlags.Primitive); - return reduceLeft((type).types, (facts, t) => containsPrimitive && getBaseConstraintOrType(t).flags & TypeFlags.Object ? facts : facts & getTypeFacts(t), TypeFacts.All); + ignoreObjects ||= maybeTypeOfKind(type, TypeFlags.Primitive); + return reduceLeft((type).types, (facts, t) => facts & getTypeFacts(t, ignoreObjects), TypeFacts.All); } return TypeFacts.All; }