Promise Truthiness Check Issues
#43071
@RyanCavanaugh took notes for this section:
- Non-strict class property initialization means some things declared truthily might actually not be
- 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
lets 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
#43183
function f<T extends "a" | "b">(x: T) {
if (x === "a") {
x // ?
takeA(x) // this should work
}
else {
x // ?
}
}
-
Could imagine x has type T & "a", but then creates huge intersections!
- Especially in the
else case.
-
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 a T extends Some | Union, when x is in a position where it's contextually typed by C, and C doesn't have any direct generic types
- then we can narrow
x using the constraint of T.
-
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 to x, so it just gets type T (which is un-narrowed).
-
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).
-
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
#43131
-
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 as T & (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 to T & "hello".
-
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
#41478
- 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 to any or unknown?
- The notes a few lines above already say
unknown so I've assumed unknown. 😅
- Yes, that's the only safe thing, don't add new holes in the system.
Promise Truthiness Check Issues
#43071
@RyanCavanaugh took notes for this section:
lets accesed in a callback (where we don't warn on lack of initialization)Narrowing Generic Types Constrained to Unions in Control Flow Analysis
#43183
Could imagine
xhas typeT & "a", but then creates huge intersections!elsecase.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
So idea: if an
xis/contains aT extends Some | Union, whenxis in a position where it's contextually typed byC, andCdoesn't have any direct generic typesxusing the constraint ofT.This makes us more complete, but adds surpises when you start moving code around.
ydoesn't provide a contextual type tox, so it just gets typeT(which is un-narrowed).Do we actually cache?
xwith respect to both its declared type (T extends Union) and its declared type's constraint (Unionitself).Last time we leveraged contextual typing with CFA, we ended up with cycles. Dealt with?
getConstraintForLocationusual place, but for identifiers, property accesses, etc.,getConstraintForReference.Is there a reason why the constraint has to be a union before we consider narrowing?
Narrowing Intersections of Primitives & Generics
#43131
When we have union types that sit "below" intersection types, we go wrong in our reasoning. Happens when you have generics constrained to unions.
T extends string | numbercan be seen asT & (string | number).T & (string | number) & "hello", but but it's hard to normalize this after the fact.T & string & "hello" | T & number & "hello"which would simplify toT & "hello".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#41478
"foo" in xis something like a type guard that creates(typeof x) & Record<"foo", unknown>.Recordmap toanyorunknown?unknownso I've assumedunknown. 😅