Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

more precise type facts for intersection #47282

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 180 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<string, TypeFacts> = new Map(getEntries({
Expand Down Expand Up @@ -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) ?
Expand Down Expand Up @@ -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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignoreObjects is only going to be true if we are in the middle of computing type facts for an intersection type. In this case, the neutral value returned by getTypeFacts can't be TypeFacts.All (as it was before), because we are oring the type facts. So it should be TypeFacts.None.

}
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
gabritto marked this conversation as resolved.
Show resolved Hide resolved
: isZero
? TypeFacts.ZeroBigIntAlwaysFalse
: TypeFacts.NonZeroBigIntAlwaysFalse;
gabritto marked this conversation as resolved.
Show resolved Hide resolved
}
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) {
Expand Down
15 changes: 15 additions & 0 deletions tests/baselines/reference/narrowingTypeof.js
Original file line number Diff line number Diff line change
@@ -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;
26 changes: 26 additions & 0 deletions tests/baselines/reference/narrowingTypeof.symbols
Original file line number Diff line number Diff line change
@@ -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))

29 changes: 29 additions & 0 deletions tests/baselines/reference/narrowingTypeof.types
Original file line number Diff line number Diff line change
@@ -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; }

27 changes: 27 additions & 0 deletions tests/baselines/reference/narrowingTypeofFunction.js
Original file line number Diff line number Diff line change
@@ -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;
}
};
27 changes: 27 additions & 0 deletions tests/baselines/reference/narrowingTypeofFunction.symbols
Original file line number Diff line number Diff line change
@@ -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))
}
}
28 changes: 28 additions & 0 deletions tests/baselines/reference/narrowingTypeofFunction.types
Original file line number Diff line number Diff line change
@@ -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
}
}
10 changes: 10 additions & 0 deletions tests/cases/compiler/narrowingTypeof.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading