-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Narrowing does not work when done via a predicate that narrows via mapped types #47283
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
Comments
Narrowing a union through a UDTG goes through this algorithm (roughly; this is from memory/experimentally-obtained):
I will name this behavior "filter-first" Not using this algorithm produces counterintuitive and unhelpful results type Cat = { meow: any };
type Dog = { woof: any };
type Chipped = { id: number };
declare function isCat(c: any): c is Cat;
declare let cd: Cat | Dog;
if (isCat(cd)) {
cd;
// ^?
// Desired & actual cd: Cat
// Without filter-first: (Cat) | (Cat & Dog)
}
declare function isChipped(c: any): c is Chipped;
if (isChipped(cd)) {
// Desired and actual cd: (Cat | Dog) & Chipped
// Without filter-first: never
} |
I get the need for pragmatism but I think this approach is a little sub-optimal. Instead of "filter-first"ing what we could do is intersect the type to be narrowed with the narrowed type and then pragmatically make (Cat | Dog) & Cat = (Cat & Cat) | (Dog & Cat) = Cat | never = Cat // desired result
(Cat | Dog) & Chipped // desired result But maybe this will bring up other problems idk haha. Actually I didn't have this exact issue posted, my issue was this failing test but turns out my So basically I'm fine with the current filter-filter approach haha, but I still think it might be a little sub-optimal. |
Intersection cannot produce |
Yes I'm aware hence "pragmatically" produce Also I realized making intersection of non-overlapping types // https://tsplay.dev/WG6x2m
type Test0 = DevanshNarrow<Cat | Dog, Cat>
// Devansh's narrow: Cat
// TypeScript's narrow: Cat
type Test1 = DevanshNarrow<Cat | Dog, Chipped>
// Devansh's narrow: (Cat | Dog) & Chipped
// TypeScript's narrow: (Cat | Dog) & Chipped
type Test2 = DevanshNarrow<
{ a: string | undefined } | { b: string },
{ a: string } | { b: string }
>
// Devansh's narrow:
// | ({ a: string | undefined } & { a: string })
// | ({ b: string } & { b: string })
//
// TypeScript's narrow:
// { b: string }
type DevanshNarrow
< T
, N
, ShouldBePragmatic =
( N extends unknown
? N extends T ? true : false
: never
) extends false
? false
: true
> =
ShouldBePragmatic extends true
? T extends unknown
? N extends unknown
? keyof T & keyof N extends never
? never
: T & N
: never
: never
: T & N
interface Cat { meow: any }
interface Dog { woof: any }
interface Chipped { id: number } Do you have cases where this algorithm would produce undesired results? I hope you see that this algorithm isn't arbitrary, narrowing by definition is intersecting narrowed with to-narrow and hence |
Also, it's important to note that the const foo = (x: { a: string | undefined } | { b: string }) => {
if (areValuesDefined(x)) {
console.log(x.b.toUpperCase())
}
}
const areValuesDefined = <T>(x: T): x is { [K in keyof T]: Exclude<T[K], undefined> } =>
Object.values(x).every(x => x !== undefined)
foo({ a: "hello" }) // TypeError: Cannot read properties of undefined (reading 'toUpperCase') With my narrowing algorithm this would have not compiled. |
@RyanCavanaugh should I open a new issue with feature request template if I want this to be considered or discussed? (I'm looking for cases where my algorithm would produce undesired results) |
@devanshj yes, new issue please. I'm not quite clear on what you're proposing. |
Done #47389 |
Bug Report
π Search Terms
Type predicate, mapped types, narrowing
π Version & Regression Information
Tried with v4.3.5
β― Playground Link
Playground link
π» Code
π Actual behavior
The type of
x
, inside theif
block, is{ b: string }
π Expected behavior
The type of
x
, inside theif
block, should have been{ a: string } | { b: string }
.Also the quickinfo of
areValueDefined
is inconsistent with the what actually the narrowed type is. The quickinfo shows the expected type.Also the narrowing
{ b: string }
is particularly weird, even if it somehow failed it should have kept the type as it is.And the narrowing does work in this case...
The text was updated successfully, but these errors were encountered: