Open
Description
π Search Terms
T to null
π Version & Regression Information
- This is a type narrowing issue
- This is the behavior in every version I tried
β― Playground Link
π» Code
function isNull<T>(value: T) {
if (value === null) {
value // T, should be null
}
return value === null;
}
π Actual behavior
value is T inside the if block of value === null (explicit comparison)
π Expected behavior
value should be of type null inside the if block of value === null
Additional information about the issue
No response
Metadata
Metadata
Assignees
Labels
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
jcalz commentedon Mar 16, 2024
This is sort of a duplicate of #13995 except that that is closed because it's largely fixed.
Note that the fact that
value
is still of typeT
isn't itself a problem. It's thatvalue
is not also constrained bynull
. That is, the real problem is in theconst v: null = value
line in:#13995 was fixed by #43183 and specifically only for the case where the generic type parameter is constrained to a union. So if you want to work around it today you could constrain
T
to{} | null | undefined
, which is a near-synonym ofunknown
, the implicit constraint of an unconstrained type parameter:Playground link
Andarist commentedon Mar 16, 2024
The negated branch gets nicely narrowed down. The observed behavior is quite surprising here (TS playground):
Andarist commentedon Mar 17, 2024
There are 2 mechanisms at play here and in related cases.
getAdjustedTypeWithFacts
focuses today on narrowing negated cases (TypeFacts.NEUndefined
,TypeFacts.NENull
, etc). That's why the other branch above works just fine. It's somewhat special because this kind of logic often can't be expressed easily by intersecting the current type with something else or by discriminating the original constraints etc. You need to handle those cases in a special way here - when something is not null then it has to be either{}
orundefined
. This has those kind of rules here.Fixing it here, in a somewhat trivial way, has almost not downsides:
git diff
However, it kinda creates some extra type identities and the control flow isn't able to "merge" those types back to just
T
when they both branches join etc.The other mechanism - the one that handles the case when
T extends {} | null | undefined
- isgetNarrowableTypeForReference
. It avoid the above problem by substituting constraints in constraints positions. Treating an unconstrained type parameter as constrained tounknown
(or rather to{} | null | undefined
akaunknownUnionType
) could fix this here. However, I found that this has some - IMHO - negative effects on printed errors. So at the very least, some extra work around that would be done here. I also had other problem when following this route but I didn't yet have the time to analyze them.RyanCavanaugh commentedon Mar 18, 2024
This is a big deal, actually. If this invariant doesn't hold, then you get "spooky action at a distance" bugs where adding an
if
statement causes effects in later code outside the CFA path of theif
Any fix here needs to not have downsides. I'd really prefer there be more concrete upsides too; at the point you've narrowed a value to a unit type, it's not clear why you'd write the identifier over the value literal (i.e. why not just write
acceptsNull(null);
?)danvk commentedon Mar 26, 2024
This issue results in some odd behavior with inferred type predicates:
in particular see this tweet for the source of this issue.
I'd guess that checking for non-null/undefined is the more common case, so it's nice that type predicates are at least inferred in that direction.