diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ae7b16234fead..747ea825e8628 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -74,7 +74,7 @@ namespace ts { TypeofNEString = 1 << 8, // typeof x !== "string" TypeofNENumber = 1 << 9, // typeof x !== "number" TypeofNEBigInt = 1 << 10, // typeof x !== "bigint" - TypeofNEBoolean = 1 << 11, // typeof x !== "boolean" + TypeofNEBoolean = 1 << 11, // typeof x !== "boolean" TypeofNESymbol = 1 << 12, // typeof x !== "symbol" TypeofNEObject = 1 << 13, // typeof x !== "object" TypeofNEFunction = 1 << 14, // typeof x !== "function" @@ -134,6 +134,60 @@ namespace ts { EmptyObjectStrictFacts = All & ~(EQUndefined | EQNull | EQUndefinedOrNull), AllTypeofNE = TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | NEUndefined, EmptyObjectFacts = All, + // Facts that are always false (i.e. false for all values of that type), to be used in `getIntersectionTypeFacts`. + // String + StringAlwaysFalse = TypeofEQNumber | TypeofEQBigInt | TypeofEQBoolean | TypeofEQSymbol | TypeofEQObject | TypeofEQFunction | TypeofEQHostObject, + StringStrictAlwaysFalse = StringAlwaysFalse | TypeofNEString | EQUndefined | EQNull | EQUndefinedOrNull, + // Empty string + EmptyStringAlwaysFalse = StringAlwaysFalse, + EmptyStringStrictAlwaysFalse = StringStrictAlwaysFalse | Truthy, + // Non-empty string + NonEmptyStringAlwaysFalse = StringAlwaysFalse, + NonEmptyStringStrictAlwaysFalse = StringStrictAlwaysFalse | Falsy, + // Number + NumberAlwaysFalse = TypeofEQString | TypeofEQBigInt | TypeofEQBoolean | TypeofEQSymbol | TypeofEQObject | TypeofEQFunction | TypeofEQHostObject, + NumberStrictAlwaysFalse = NumberAlwaysFalse | TypeofNENumber | EQUndefined | EQNull | EQUndefinedOrNull, + // Zero number + ZeroNumberAlwaysFalse = NumberAlwaysFalse, + ZeroNumberStrictAlwaysFalse = NumberStrictAlwaysFalse | Truthy, + // Non-zero number + NonZeroNumberAlwaysFalse = NumberAlwaysFalse, + NonZeroNumberStrictAlwaysFalse = NumberStrictAlwaysFalse | Falsy, + // Big int + BigIntAlwaysFalse = TypeofEQString | TypeofEQNumber | TypeofEQBoolean | TypeofEQSymbol | TypeofEQObject | TypeofEQFunction | TypeofEQHostObject, + BigIntStrictAlwaysFalse = BigIntAlwaysFalse | TypeofNEBigInt | EQUndefined | EQNull | EQUndefinedOrNull, + // Zero big int + ZeroBigIntAlwaysFalse = BigIntAlwaysFalse, + ZeroBigIntStrictAlwaysFalse = BigIntStrictAlwaysFalse | Truthy, + // Non-zero big int + NonZeroBigIntAlwaysFalse = BigIntAlwaysFalse, + NonZeroBigIntStrictAlwaysFalse = BigIntStrictAlwaysFalse | Falsy, + // Boolean + BooleanAlwaysFalse = TypeofEQString | TypeofEQNumber | TypeofEQBigInt | TypeofEQSymbol | TypeofEQObject | TypeofEQFunction | TypeofEQHostObject, + BooleanStrictAlwaysFalse = BooleanAlwaysFalse | TypeofNEBoolean | EQUndefined | EQNull | EQUndefinedOrNull, + // Boolean-like + // False + FalseAlwaysFalse = BooleanAlwaysFalse, + FalseStrictAlwaysFalse = BooleanStrictAlwaysFalse | Truthy, + // True + TrueAlwaysFalse = BooleanAlwaysFalse, + TrueStrictAlwaysFalse = BooleanStrictAlwaysFalse | Falsy, + // Undefined + UndefinedAlwaysFalse = TypeofEQSymbol | TypeofEQNumber | TypeofEQBigInt | TypeofEQBoolean | TypeofEQSymbol | TypeofEQObject | TypeofEQFunction | TypeofEQHostObject | NEUndefined | NEUndefinedOrNull | EQNull | Truthy, + // Null + NullAlwaysFalse = TypeofNEObject | TypeofEQString | TypeofEQNumber | TypeofEQBigInt | TypeofEQBoolean | TypeofEQSymbol | TypeofEQFunction | TypeofEQHostObject | NENull | NEUndefinedOrNull | EQUndefined | Truthy, + // Symbol + SymbolAlwaysFalse = TypeofEQString | TypeofEQNumber | TypeofEQBigInt | TypeofEQBoolean | TypeofEQObject | TypeofEQFunction | TypeofEQHostObject, + SymbolStrictAlwaysFalse = SymbolAlwaysFalse | TypeofNESymbol | EQUndefined | EQNull | EQUndefinedOrNull, + // Function + FunctionAlwaysFalse = TypeofEQSymbol | TypeofEQNumber | TypeofEQBigInt | TypeofEQBoolean | TypeofEQSymbol | TypeofEQObject | TypeofEQHostObject, + FunctionStrictAlwaysFalse = FunctionAlwaysFalse | TypeofNEFunction | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, + // Object + ObjectAlwaysFalse = TypeofEQSymbol | TypeofEQNumber | TypeofEQBigInt | TypeofEQBoolean | TypeofEQSymbol, + ObjectStrictAlwaysFalse = ObjectAlwaysFalse | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, + // Empty object + EmptyObjectAlwaysFalse = None, + EmptyObjectStrictAlwaysFalse = EQUndefined | EQNull | EQUndefinedOrNull, } const typeofEQFacts: ReadonlyESMap = new Map(getEntries({ @@ -22935,7 +22989,10 @@ namespace ts { (type === falseType || type === regularFalseType) ? TypeFacts.FalseStrictFacts : TypeFacts.TrueStrictFacts : (type === falseType || type === regularFalseType) ? TypeFacts.FalseFacts : TypeFacts.TrueFacts; } - if (flags & TypeFlags.Object && !ignoreObjects) { + if (flags & TypeFlags.Object) { + if (ignoreObjects) { + return TypeFacts.None; + } return getObjectFlags(type) & ObjectFlags.Anonymous && isEmptyObjectType(type as ObjectType) ? strictNullChecks ? TypeFacts.EmptyObjectStrictFacts : TypeFacts.EmptyObjectFacts : isFunctionObjectType(type as ObjectType) ? @@ -22964,13 +23021,132 @@ namespace ts { if (flags & TypeFlags.Union) { return reduceLeft((type as UnionType).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. + ignoreObjects ||= maybeTypeOfKind(type, TypeFlags.Primitive); + return getIntersectionTypeFacts(type as IntersectionType, ignoreObjects); + } + return TypeFacts.All; + } + + function getIntersectionTypeFacts(type: IntersectionType, ignoreObjects: boolean): TypeFacts { + let alwaysFalse = TypeFacts.None; + let facts = TypeFacts.None; + for (const t of type.types) { + facts |= getTypeFacts(t, ignoreObjects); + alwaysFalse |= getAlwaysFalseTypeFacts(t, ignoreObjects); + } + + return facts & ~alwaysFalse; + } + + function getAlwaysFalseTypeFacts(type: Type, ignoreObjects: boolean): TypeFacts { + const flags = type.flags; + if (flags & TypeFlags.String) { + return strictNullChecks ? TypeFacts.StringStrictAlwaysFalse : TypeFacts.StringAlwaysFalse; + } + if (flags & TypeFlags.StringLiteral) { + const isEmpty = (type as StringLiteralType).value === ""; + return strictNullChecks + ? isEmpty + ? TypeFacts.EmptyStringStrictAlwaysFalse + : TypeFacts.NonEmptyStringStrictAlwaysFalse + : isEmpty + ? TypeFacts.EmptyStringAlwaysFalse + : TypeFacts.NonEmptyStringAlwaysFalse; + } + if (flags & (TypeFlags.Number | TypeFlags.Enum)) { + return strictNullChecks ? TypeFacts.NumberStrictAlwaysFalse : TypeFacts.NumberAlwaysFalse; + } + if (flags & TypeFlags.NumberLiteral) { + const isZero = (type as NumberLiteralType).value === 0; + return strictNullChecks + ? isZero + ? TypeFacts.ZeroNumberStrictAlwaysFalse + : TypeFacts.NonZeroNumberStrictAlwaysFalse + : isZero + ? TypeFacts.ZeroNumberAlwaysFalse + : TypeFacts.NonZeroNumberAlwaysFalse; + } + if (flags & TypeFlags.BigInt) { + return strictNullChecks ? TypeFacts.BigIntStrictAlwaysFalse : TypeFacts.BigIntAlwaysFalse; + } + if (flags & TypeFlags.BigIntLiteral) { + const isZero = isZeroBigInt(type as BigIntLiteralType); + return strictNullChecks + ? isZero + ? TypeFacts.ZeroBigIntStrictAlwaysFalse + : TypeFacts.NonZeroBigIntStrictAlwaysFalse + : isZero + ? TypeFacts.ZeroBigIntAlwaysFalse + : TypeFacts.NonZeroBigIntAlwaysFalse; + } + if (flags & TypeFlags.Boolean) { + return strictNullChecks ? TypeFacts.BooleanStrictAlwaysFalse : TypeFacts.BooleanAlwaysFalse; + } + if (flags & TypeFlags.BooleanLike) { + return strictNullChecks + ? (type === falseType || type === regularFalseType) + ? TypeFacts.FalseStrictAlwaysFalse + : TypeFacts.TrueStrictAlwaysFalse + : (type === falseType || type === regularFalseType) + ? TypeFacts.FalseAlwaysFalse + : TypeFacts.TrueAlwaysFalse; + } + if (flags & TypeFlags.Object) { + if (ignoreObjects) { + return TypeFacts.None; + } + return getObjectFlags(type) & ObjectFlags.Anonymous && isEmptyObjectType(type as ObjectType) + ? strictNullChecks + ? TypeFacts.EmptyObjectStrictAlwaysFalse + : TypeFacts.EmptyObjectAlwaysFalse + : isFunctionObjectType(type as ObjectType) + ? strictNullChecks + ? TypeFacts.FunctionStrictAlwaysFalse + : TypeFacts.FunctionAlwaysFalse + : strictNullChecks + ? TypeFacts.ObjectStrictAlwaysFalse + : TypeFacts.ObjectAlwaysFalse; + } + if (flags & (TypeFlags.Void | TypeFlags.Undefined)) { + return TypeFacts.UndefinedAlwaysFalse; + } + if (flags & TypeFlags.Null) { + return TypeFacts.NullAlwaysFalse; + } + if (flags & TypeFlags.ESSymbolLike) { + return strictNullChecks ? TypeFacts.SymbolStrictAlwaysFalse : TypeFacts.SymbolAlwaysFalse; + } + if (flags & TypeFlags.NonPrimitive) { + return strictNullChecks ? TypeFacts.ObjectStrictAlwaysFalse : TypeFacts.ObjectAlwaysFalse; + } + if (flags & TypeFlags.Never) { + return TypeFacts.None; + } + if (flags & TypeFlags.Instantiable) { + return !isPatternLiteralType(type) + ? getAlwaysFalseTypeFacts(getBaseConstraintOfType(type) || unknownType, ignoreObjects) + : strictNullChecks + ? TypeFacts.NonEmptyStringStrictAlwaysFalse + : TypeFacts.NonEmptyStringAlwaysFalse; + } + if (flags & TypeFlags.Union) { + const types = (type as UnionType).types; + if (types.length) { + return reduceLeft(types, (facts, t) => facts & getAlwaysFalseTypeFacts(t, ignoreObjects), TypeFacts.All); + } + return 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. ignoreObjects ||= maybeTypeOfKind(type, TypeFlags.Primitive); - return reduceLeft((type as UnionType).types, (facts, t) => facts & getTypeFacts(t, ignoreObjects), TypeFacts.All); + return reduceLeft((type as IntersectionType).types, (facts, t) => facts | getAlwaysFalseTypeFacts(t, ignoreObjects), TypeFacts.None); } - return TypeFacts.All; + + return TypeFacts.None; } function getTypeWithFacts(type: Type, include: TypeFacts) { diff --git a/tests/baselines/reference/narrowingTypeof.js b/tests/baselines/reference/narrowingTypeof.js new file mode 100644 index 0000000000000..25d7e2d9ec59b --- /dev/null +++ b/tests/baselines/reference/narrowingTypeof.js @@ -0,0 +1,15 @@ +//// [narrowingTypeof.ts] +type __String = (string & { __escapedIdentifier: void }) | (void & { __escapedIdentifier: void }); + +declare const s: __String; +declare let t: string | number | undefined; + +declare function assert(e: unknown): asserts e; + +assert(typeof s === "string"); +t = s; + + +//// [narrowingTypeof.js] +assert(typeof s === "string"); +t = s; diff --git a/tests/baselines/reference/narrowingTypeof.symbols b/tests/baselines/reference/narrowingTypeof.symbols new file mode 100644 index 0000000000000..51343b592c44c --- /dev/null +++ b/tests/baselines/reference/narrowingTypeof.symbols @@ -0,0 +1,26 @@ +=== tests/cases/compiler/narrowingTypeof.ts === +type __String = (string & { __escapedIdentifier: void }) | (void & { __escapedIdentifier: void }); +>__String : Symbol(__String, Decl(narrowingTypeof.ts, 0, 0)) +>__escapedIdentifier : Symbol(__escapedIdentifier, Decl(narrowingTypeof.ts, 0, 27)) +>__escapedIdentifier : Symbol(__escapedIdentifier, Decl(narrowingTypeof.ts, 0, 68)) + +declare const s: __String; +>s : Symbol(s, Decl(narrowingTypeof.ts, 2, 13)) +>__String : Symbol(__String, Decl(narrowingTypeof.ts, 0, 0)) + +declare let t: string | number | undefined; +>t : Symbol(t, Decl(narrowingTypeof.ts, 3, 11)) + +declare function assert(e: unknown): asserts e; +>assert : Symbol(assert, Decl(narrowingTypeof.ts, 3, 43)) +>e : Symbol(e, Decl(narrowingTypeof.ts, 5, 24)) +>e : Symbol(e, Decl(narrowingTypeof.ts, 5, 24)) + +assert(typeof s === "string"); +>assert : Symbol(assert, Decl(narrowingTypeof.ts, 3, 43)) +>s : Symbol(s, Decl(narrowingTypeof.ts, 2, 13)) + +t = s; +>t : Symbol(t, Decl(narrowingTypeof.ts, 3, 11)) +>s : Symbol(s, Decl(narrowingTypeof.ts, 2, 13)) + diff --git a/tests/baselines/reference/narrowingTypeof.types b/tests/baselines/reference/narrowingTypeof.types new file mode 100644 index 0000000000000..a9035c4bfd8c9 --- /dev/null +++ b/tests/baselines/reference/narrowingTypeof.types @@ -0,0 +1,29 @@ +=== tests/cases/compiler/narrowingTypeof.ts === +type __String = (string & { __escapedIdentifier: void }) | (void & { __escapedIdentifier: void }); +>__String : __String +>__escapedIdentifier : void +>__escapedIdentifier : void + +declare const s: __String; +>s : __String + +declare let t: string | number | undefined; +>t : string | number + +declare function assert(e: unknown): asserts e; +>assert : (e: unknown) => asserts e +>e : unknown + +assert(typeof s === "string"); +>assert(typeof s === "string") : void +>assert : (e: unknown) => asserts e +>typeof s === "string" : boolean +>typeof s : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>s : __String +>"string" : "string" + +t = s; +>t = s : string & { __escapedIdentifier: void; } +>t : string | number +>s : string & { __escapedIdentifier: void; } + diff --git a/tests/baselines/reference/narrowingTypeofFunction.js b/tests/baselines/reference/narrowingTypeofFunction.js new file mode 100644 index 0000000000000..f4a1114d4b735 --- /dev/null +++ b/tests/baselines/reference/narrowingTypeofFunction.js @@ -0,0 +1,27 @@ +//// [narrowingTypeofFunction.ts] +type Meta = { foo: string } +interface F { (): string } + +const x = (a: (F & Meta) | string) => { + if (typeof a === "function") { + // ts.version >= 4.3.5: never -- unexpected + // ts.version <= 4.2.3: F & Meta -- expected + a; + } + else { + a; + } +} + +//// [narrowingTypeofFunction.js] +"use strict"; +var x = function (a) { + if (typeof a === "function") { + // ts.version >= 4.3.5: never -- unexpected + // ts.version <= 4.2.3: F & Meta -- expected + a; + } + else { + a; + } +}; diff --git a/tests/baselines/reference/narrowingTypeofFunction.symbols b/tests/baselines/reference/narrowingTypeofFunction.symbols new file mode 100644 index 0000000000000..4d9eddf002099 --- /dev/null +++ b/tests/baselines/reference/narrowingTypeofFunction.symbols @@ -0,0 +1,27 @@ +=== tests/cases/compiler/narrowingTypeofFunction.ts === +type Meta = { foo: string } +>Meta : Symbol(Meta, Decl(narrowingTypeofFunction.ts, 0, 0)) +>foo : Symbol(foo, Decl(narrowingTypeofFunction.ts, 0, 13)) + +interface F { (): string } +>F : Symbol(F, Decl(narrowingTypeofFunction.ts, 0, 27)) + +const x = (a: (F & Meta) | string) => { +>x : Symbol(x, Decl(narrowingTypeofFunction.ts, 3, 5)) +>a : Symbol(a, Decl(narrowingTypeofFunction.ts, 3, 11)) +>F : Symbol(F, Decl(narrowingTypeofFunction.ts, 0, 27)) +>Meta : Symbol(Meta, Decl(narrowingTypeofFunction.ts, 0, 0)) + + if (typeof a === "function") { +>a : Symbol(a, Decl(narrowingTypeofFunction.ts, 3, 11)) + + // ts.version >= 4.3.5: never -- unexpected + // ts.version <= 4.2.3: F & Meta -- expected + a; +>a : Symbol(a, Decl(narrowingTypeofFunction.ts, 3, 11)) + } + else { + a; +>a : Symbol(a, Decl(narrowingTypeofFunction.ts, 3, 11)) + } +} diff --git a/tests/baselines/reference/narrowingTypeofFunction.types b/tests/baselines/reference/narrowingTypeofFunction.types new file mode 100644 index 0000000000000..2a417d435af6a --- /dev/null +++ b/tests/baselines/reference/narrowingTypeofFunction.types @@ -0,0 +1,28 @@ +=== tests/cases/compiler/narrowingTypeofFunction.ts === +type Meta = { foo: string } +>Meta : Meta +>foo : string + +interface F { (): string } + +const x = (a: (F & Meta) | string) => { +>x : (a: (F & Meta) | string) => void +>(a: (F & Meta) | string) => { if (typeof a === "function") { // ts.version >= 4.3.5: never -- unexpected // ts.version <= 4.2.3: F & Meta -- expected a; } else { a; }} : (a: (F & Meta) | string) => void +>a : string | (F & Meta) + + if (typeof a === "function") { +>typeof a === "function" : boolean +>typeof a : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>a : string | (F & Meta) +>"function" : "function" + + // ts.version >= 4.3.5: never -- unexpected + // ts.version <= 4.2.3: F & Meta -- expected + a; +>a : F & Meta + } + else { + a; +>a : string + } +} diff --git a/tests/cases/compiler/narrowingTypeof.ts b/tests/cases/compiler/narrowingTypeof.ts new file mode 100644 index 0000000000000..461ab6780cba5 --- /dev/null +++ b/tests/cases/compiler/narrowingTypeof.ts @@ -0,0 +1,10 @@ + +type __String = (string & { __escapedIdentifier: void }) | (void & { __escapedIdentifier: void }); + +declare const s: __String; +declare let t: string | number | undefined; + +declare function assert(e: unknown): asserts e; + +assert(typeof s === "string"); +t = s; diff --git a/tests/cases/compiler/narrowingTypeofFunction.ts b/tests/cases/compiler/narrowingTypeofFunction.ts new file mode 100644 index 0000000000000..4cd5eaab2efff --- /dev/null +++ b/tests/cases/compiler/narrowingTypeofFunction.ts @@ -0,0 +1,15 @@ +// @strict: true + +type Meta = { foo: string } +interface F { (): string } + +const x = (a: (F & Meta) | string) => { + if (typeof a === "function") { + // ts.version >= 4.3.5: never -- unexpected + // ts.version <= 4.2.3: F & Meta -- expected + a; + } + else { + a; + } +} \ No newline at end of file