-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Narrow discriminated union based on generic function arguments #46899
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
The example as written is pretty tough; I'm not sure how we could do that. A possibly tractable (and functionally equivalent version) would be const narrowingFunction = <T extends Things, K extends T["key"]>(
thing: T,
key: K,
callback: (thing: T & { key: K }) => void
) => {
if (thing.key === key) {
callback(thing); // Type 'Thing' is not assignable to type 'NarrowedThing<T, K>'.(2345)
}
}; The relationship between the comparison and the narrowing is more clear here -- |
@RyanCavanaugh I've just played with that in the playground; I'm happy that this satisfies the use case I was trying to articulate :) |
@RyanCavanaugh if I wanted to have a go at implementing this myself, what areas of the codebase would I need to I appreciate that it might be beyond me as a new contributor, but I'd be interested in at least having a poke to see if it's doable |
I'd also like to have a go at this if possible, or at least get an update on whether this is on the roadmap. Here's a similar case: // Pretty simple discriminated union...
type Circle = {
type: 'circle';
radius: number;
}
type Square = {
type: 'square';
width: number;
}
type Shape = Circle | Square;
// What if we want a generic factory?
// It would be really cool if the compiler could infer this return type from the function implementation
// Even with the explicit conditional return type, there are problems...
const shape = <T extends Shape['type']>(type: T, size: number): T extends 'circle' ? Circle : T extends 'square' ? Square : Shape => {
switch(type) {
case 'circle':
// Ideally the compiler would be smart enough to narrow T here
return { type: 'circle', radius: size };
case 'square':
// and here
return { type: 'square', width: size };
default:
throw new Error ("invalid shape type");
}
}
// From the outside, though, this seems to work correctly:
// Basic usage results in correct inference/typing
let circle = shape('circle', 5);
let square = shape('square', 5);
// Reasonably infers union type if key is uncertain
let isCircle = false;
let maybeCircle = shape(isCircle ? 'circle' : 'square', 5); Any thoughts of the feasibility of making the inferred function type behave better here along with fixing the issue? |
It kind of seems to me like the original code should work, but the issue can be worked around using the type NarrowedThing<K> = Things & { key: K };
const narrowingFunction = <K extends Things["key"]>(
thing: NarrowedThing<K>,
key: K,
callback: (thing: NarrowedThing<K>) => void
) => {
if (thing.key === key) {
callback(thing);
}
}; On the other hand, the same idea doesn't work on @Oblarg's example. This still fails: const shape = <T extends Shape['type']>(type: T, size: number): Shape & { type: T } => {
switch (type) {
case 'circle':
return { type, radius: size };
case 'square':
return { type, width: size };
default:
throw new Error("invalid shape type");
}
} I think the original example also fails for another unrelated reason, because the switch statement here also breaks, but it works if the literal variables in the return statements are replaced with const classify = <T extends Shape['type']>(type: T): T => {
switch (type) {
case 'circle':
return 'circle';
case 'square':
return 'square';
default:
throw new Error("invalid shape type");
}
} |
On the third hand, I think the factory function example makes more sense using function overloading: function shape(type: "circle", size: number): Circle;
function shape(type: "square", size: number): Square;
function shape(type: Shape["type"], size: number): Shape {...} This loses some generality, but I don't think it's useful generality, because function shape(type: "circle", size: number): Circle;
function shape(type: "square", size: number): Square;
function shape(type: "rectangle", length: number, width: number): Rectangle;
function shape(type: Shape["type"], size1: number, size2?: number): Shape {...} |
Suggestion
π Search Terms
narrowing, generic, arguments
β Viability Checklist
β Suggestion
In a few different projects, I've wanted to write a wrapper function that takes as arguments
To be more specific, I wanted to be able to do something like this:
Now it may be that my understanding of TypeScript is too limited to understand what is possible here, but it seems to me that it would be correct for
thing
to be automatically narrowed to something like myNarrowedThings
typeπ Motivating Example
I recently tried to write a small library to simplify the handling of AWS EventBridge events in a way that allowed you to provide handler functions and cleanly narrowed the types. The API looked a little like this:
Under the hood, this API is just an abstraction over the simple 3 argument function I describe above. In order to narrow the type I had to write a custom type guard as there appeared to be no way for TypeScript to narrow arguments that were typed with generics.
π» Use Cases
I've also wanted something like this when handling redux actions within a reducer - I can see libraries such as
ts-pattern
making use of this under the hood. As I note above, it is perfectly possible to implement this with a custom type guard, but it seems to me that everything that is needed to safely narrow the type is known at build time so a custom guard shouldn't be necessary.The text was updated successfully, but these errors were encountered: