Closed
Description
Inference Change Between Unions of undefined
Here's a minimal break.
type B = { foo: string, };
type D = { foo: string, bar: number };
declare function equals<T>(a: T, b: T): boolean;
function f(b: B, d: D | undefined) {
// Used to work, now infers `B` and fails.
if (equals(b, d)) {
}
}
- Bloomberg uncovered this from TS 4.8 Beta.
- Why did this used to work?
- We would get all the candidates, toss out the
undefined
, try to find the common super type, and re-addundefined
. - Recent PR changed this (Improve intersection reduction and CFA for truthy, equality, and typeof checks #49119), but Ensure common supertype retains nullability in inference #49981 re-adds some of it.
- We would get all the candidates, toss out the
- Seems like we've found a middle-ground.
- Didn't Improve intersection reduction and CFA for truthy, equality, and typeof checks #49119 have
- But the check for falsy flags went away (
getFalsyFlagsOfTypes
is a recursive fetch of flags)
- But the check for falsy flags went away (
- Is this a desirable fix? Seems like a heuristic.
- You can keep going and find use-cases where it becomes more and more arguable that you want to let things work.
- Regardless, need to fix before 4.8.
Narrowing Changes Against Top-ish Types
-
The
isMap
type predicate now narrows values to aMap<...> | ReadonlyMap<...>
instead ofMap<...>
if that's what you already had. -
Introduced by Improve narrowing logic for
instanceof
, type predicate functions, and assertion functions #49625 -
Start with this example.
type Falsy = false | 0 | 0n | '' | null | undefined; declare function isFalsy(value: unknown): value is Falsy; function fx1(x: string | number | undefined) { if (isFalsy(x)) { x; // "" | 0 | undefined, previously undefined } }
-
But people depend on the old behavior.
-
This is now different
declare function isMap<T>(object: T | {}): object is T extends ReadonlyMap<any, any> ? (unknown extends T ? never : ReadonlyMap<any, any>) : Map<unknown, unknown>; declare const romor: ReadonlyMap<any, any> | Record<any, any> if (isMap(romor)) { romor; // Previously `ReadonlyMap<any, any>`, now `ReadonlyMap<any, any> | Map<any, any>` }
-
But
ReadonlyMap<...> | Map<...>
just provides all the same methods asReadonlyMap<...>
.- Are they sufficiently identical between their methods?
- Often these signatures are not, which makes them effectively uncallable.
- We think yes.
-
-
Previous behavior was weird! (see playground)
declare function isObject(value: unknown): value is Record<string, unknown>; function f1(obj: {}) { if (isObject(obj)) { // Worked, obj is narrowed to Record<string, unknown> obj["stuff"] } } function f2(obj: {} | undefined) { if (isObject(obj)) { // Doesn't work obj is not narrowed. obj["stuff"] } }
-
Now neither example works. (see playground)
-
Will need to think through these.
-
-
WeakRef
example- Very incorrect.
T
is never witnessed, andWeakRef
is supposed to be a proxy aroundT
itself.
- Very incorrect.