-
Notifications
You must be signed in to change notification settings - Fork 12.8k
more precise type facts for intersection #47282
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
Conversation
@typescript-bot test this |
Heya @andrewbranch, I've started to run the extended test suite on this PR at 2489916. You can monitor the build here. |
Heya @andrewbranch, I've started to run the perf test suite on this PR at 2489916. You can monitor the build here. Update: The results are in! |
Heya @andrewbranch, I've started to run the parallelized Definitely Typed test suite on this PR at 2489916. You can monitor the build here. |
Heya @andrewbranch, I've started to run the inline community code test suite on this PR at 2489916. You can monitor the build here. Update: The results are in! |
@andrewbranch |
@andrewbranch Here they are:Comparison Report - main..47282
System
Hosts
Scenarios
Developer Information: |
if (flags & TypeFlags.Object && !ignoreObjects) { | ||
if (flags & TypeFlags.Object) { | ||
if (ignoreObjects) { | ||
return TypeFacts.None; |
There was a problem hiding this comment.
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 or
ing the type facts. So it should be TypeFacts.None
.
I'm definitely in favor of fixing this issue, but I'm not crazy about adding 175 lines of code to do it. Couldn't we just say that certain type facts for intersections are computed as |
Fixes #45801
Brief background
When narrowing a type, we rely on
TypeFacts
flags that tell us what narrowing facts could be true for a value of a given type. The flags cover the possible results of narrowing usingtypeof
(e.g.TypeFacts.TypeofEQString
), comparsions tonull
orundefined
(e.g.TypeFacts.UndefinedOrNull
meansif (v === null || v === undefined)
could be true), or if the value is truthy or falsy (e.g.TypeFacts.Truthy
meansif (v) { ... }
could be true).To compute the type facts for a type
T
, we callgetTypeFacts(T)
.The issue
Up until v4.2, when computing type facts for an intersection type, we
or
ed the facts of each of the intersection type's component type. e.g.getTypeFacts(A & B)
=getTypeFacts(A) | getTypeFacts(B)
.Starting at v4.3, this changed to an
and
of the facts of each intersection type's component type, e.g.getTypeFacts(A & B)
=getTypeFacts(A) & getTypeFacts(B)
, to fix cases like the following:If we have
T & number
, we know the value can only be of type number (or null or undefined, in non-strict), never of type string. However, we don't know anything aboutT
, so when computing type facts forT
, we think it can be anything at all (including of type string). If weor
the type facts forT
andnumber
, then, we get thatT & number
could be string. If weand
the type facts, we knowT & number
can't be of type string, becausenumber
can't be of type string.However, in some cases we don't want an
and
of the type facts. Consider the example from issue #45801:For type
F & Meta
, we know values ofF
have to be functions, sinceF
has a call signature declared. But typeMeta
doesn't, it is a regular object. If weand
those facts, we concludeF & Meta
cannot be a function, because we don't think values ofMeta
could be functions (no call signature). So in this case, we probably want toor
the type facts.(Note: We could include
TypeFacts.TypeofEQFunction
in the flags returned for object types such asMeta
above, and keep usingand
for computing type facts of intersection types. I think we don't want to do that, since we explicitly make that distinction ingetTypeFacts
's code and it generally seems a useful distinction.)The possible fix
Considering both examples above, it seems we sometimes know a fact will always be true (i.e. true for all values of a type), e.g. values of type
T & number
can't be strings (if (typeof v === 'string')
can't be true). This contrasts with the other uses of type facts, which are facts that can be true for values of type T, but are not necessarily always true/true for all values.So that's why sometimes we want to use an
and
to compute facts of intersection types, and sometimes we want to use anor
.tl;dr
This PR changes the way we compute type facts of intersection types. For each component type, in addition to computing its type facts, we also compute which facts are always true (true for all values of that type) and which are always false.
We then
or
the regular type facts of the component types, and we alsoor
the always false facts. We then exclude the facts that are always false from the regular facts.So, in cases like
T & number
, even though when weor
the components' type facts, we include a flag such asTypeFacts.TypeofEQString
(because that's one of the flags forT
), we will delete it later becauseTypeFacts.TypeofEQString
is always false for typenumber
.Questions I had
When isIt seemsTypeFacts.TypeofEQHostObject
always false?typeof v === "xxx"
("xxx" being something other than the usual strings returned bytypeof
) can only be true ifv
is a host object (and hosts return a customtypeof
for those), or if we don't yet recognize the "xxx" in question (e.g. if there's ever a newFraction
type andtypeof v === "fraction"
). So it should always be false for known primitives at least.Should we care about contradictory types (e.g.I'm assuming we shouldn't, because existing code forstring & number
) ingetTypeFacts
orgetAlwaysFalseTypeFacts
?getTypeFacts
doesn't address this, and this looks like somethinggetIntersectionType
takes care of.