diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 6bb2aa7fe294d..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,14 +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) { - 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. + ignoreObjects ||= maybeTypeOfKind(type, TypeFlags.Primitive); + return reduceLeft((type).types, (facts, t) => facts & getTypeFacts(t, ignoreObjects), TypeFacts.All); } return TypeFacts.All; } diff --git a/tests/baselines/reference/taggedPrimitiveNarrowing.js b/tests/baselines/reference/taggedPrimitiveNarrowing.js new file mode 100644 index 0000000000000..a87ea61ddf32c --- /dev/null +++ b/tests/baselines/reference/taggedPrimitiveNarrowing.js @@ -0,0 +1,42 @@ +//// [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; +} + +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] +"use strict"; +function getHashLength(hash) { + if (typeof hash !== "string") { + throw new Error("This doesn't look like a 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] +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 new file mode 100644 index 0000000000000..50fd9bdb40caf --- /dev/null +++ b/tests/baselines/reference/taggedPrimitiveNarrowing.symbols @@ -0,0 +1,41 @@ +=== 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, --, --)) +} + +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 new file mode 100644 index 0000000000000..c26e4174b593d --- /dev/null +++ b/tests/baselines/reference/taggedPrimitiveNarrowing.types @@ -0,0 +1,49 @@ +=== 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 +} + +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 new file mode 100644 index 0000000000000..50ca3704abe89 --- /dev/null +++ b/tests/cases/compiler/taggedPrimitiveNarrowing.ts @@ -0,0 +1,18 @@ +// @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; +} + +function getHashLength2(hash: string & T): number { + if (typeof hash !== "string") { + throw new Error("This doesn't look like a hash"); + } + return hash.length; +}