Description
Promise Truthiness Check Issues
@RyanCavanaugh took notes for this section:
- Non-strict class property initialization means some things declared truthily might actually not be
- Same for array access
- But we error because we think the property is always initialized thus always truthy
- Many of these "should have" been declared optional
- Also can happen for late-initialized
let
s accesed in a callback (where we don't warn on lack of initialization) - Uncalled functions and unawaited promises should never be an error when the same identifier is referenced in the truthy block
- Unclear how to fix "probably truthy" expressions like array access, class properties, and lets in callbacks
- Fix just the obvious bugs here
Narrowing Generic Types Constrained to Unions in Control Flow Analysis
function f<T extends "a" | "b">(x: T) {
if (x === "a") {
x // ?
takeA(x) // this should work
}
else {
x // ?
}
}
-
Could imagine
x
has typeT & "a"
, but then creates huge intersections!- Especially in the
else
case.
- Especially in the
-
Talked with Pyright team, they have a similar issue.
-
They try the declared (generic) type first, then try to get the constraint and operate on that (double-check with them).
-
We do something similar with
function f<T extends Item | undefined>(x: T) { if (x) { obj.propName // works } }
-
-
So idea: if an
x
is/contains aT extends Some | Union
, whenx
is in a position where it's contextually typed byC
, andC
doesn't have any direct generic types- then we can narrow
x
using the constraint ofT
.
- then we can narrow
-
This makes us more complete, but adds surpises when you start moving code around.
function f<T extends Foo | undefined>(x: T) { if (x) { takeFoo(x); // works let y = x; takeFoo(y); // doesn't! 😬 } }
- Because the declaration of
y
doesn't provide a contextual type tox
, so it just gets typeT
(which is un-narrowed).
- Because the declaration of
-
Do we actually cache?
- Yes! And that's the cool part, our cache key incorporates both the current reference as well as the declared type. So you can cache information about
x
with respect to both its declared type (T extends Union
) and its declared type's constraint (Union
itself).
- Yes! And that's the cool part, our cache key incorporates both the current reference as well as the declared type. So you can cache information about
-
Last time we leveraged contextual typing with CFA, we ended up with cycles. Dealt with?
getConstraintForLocation
usual place, but for identifiers, property accesses, etc.,getConstraintForReference
.
-
Is there a reason why the constraint has to be a union before we consider narrowing?
- Nothing to gain if it's not a union.
Narrowing Intersections of Primitives & Generics
-
When we have union types that sit "below" intersection types, we go wrong in our reasoning. Happens when you have generics constrained to unions.
function f<T extends string | number>(x: T) { if (x === "hello") { x // want this to be `string` or `"hello"` } }
- In a sense,
T extends string | number
can be seen asT & (string | number)
. - Here, you have a situation where you want to see this as
T & (string | number) & "hello"
, but but it's hard to normalize this after the fact. - If you did, you'd end up with
T & string & "hello" | T & number & "hello"
which would simplify toT & "hello"
.
- In a sense,
-
So we've modified the comparable relationship to at least just recognize this pattern, without rewriting the types at any point.
Narrowing Record Types with in
- Idea:
"foo" in x
is something like a type guard that creates(typeof x) & Record<"foo", unknown>
.- Does this work with the join at the bottom of the CFG?
- As long as you don't do anything in the negative case - then it'll just vanish.
- "Core premise is not wonky."
- Only do this in not-a-union cases.
- Does the
Record
map toany
orunknown
?- The notes a few lines above already say
unknown
so I've assumedunknown
. 😅 - Yes, that's the only safe thing, don't add new holes in the system.
- The notes a few lines above already say