Reputation: 1093
Given the discriminated union MyUnion
, I want to call a function createMyUnionObject
with one of the type
s of MyUnion
and a value for value
which must be the correct type.
type MyUnion =
{
type: 'one'
value: string
} |
{
type: 'two',
value: number
} |
{
type: 'three',
value: boolean
};
function createMyUnionObject<
T extends MyUnion['type'],
MU extends Extract<MyUnion, { type: T }>,
V extends MU['value']
>(type: T, value: V): MU {
// Real code is conditional and may modify `value`, this is a simple example with the same problem I'm having
return { type, value };
}
If you try calling createMyUnionObject
, you'll see that TypeScript is correctly inferring the values outside the function definition. That is, createMyUnionObject('one', false)
will have the desired compile error Argument of type 'false' is not assignable to parameter of type 'string'
.
Within the function definition, however, I'm seeing Type '{ type: T; value: V; }' is not assignable to type 'MU'.
and I don't understand why - how can I construct the object so that TypeScript is satisfied that it's the correct type?
Upvotes: 0
Views: 402
Reputation: 327964
TypeScript isn't currently able to narrow the types of generic type parameters via control flow analysis. Ideally I'd love to be able to write your function like this:
function createMyUnionObject<T extends MyUnion["type"]>(
type: T,
value: Extract<MyUnion, { type: T }>["value"]
): Extract<MyUnion, { type: T }> {
return { type, value }; // error 😥
}
but we can't (note that we should only need one generic parameter... from T
you should be able to compute the value
and return types). Even though for any concrete value of type
and its corresponding value
the compiler can verify that {type, value}
is assignable to MyUnion
, it can't do it with generics.
One obstacle is that the compiler suspects that your type
might be of the full union type "one" | "two" | "three"
, and of course value
could therefore be string | number | boolean
, and then {type, value}
might not be assignable to MyUnion
:
const t = (["one", "two", "three"] as const)[Math.floor(Math.random() * 3)];
const v = (["one", 2, true] as const)[Math.floor(Math.random() * 3)];
createMyUnionObject(t, v); // uh oh
You want to tell the compiler: no, T
can only be either "one"
, or "two"
, or "three"
, but not the union of two or more of those. That is not part of the language yet but it's been requested. If that ever gets implemented it would still need to be combined with some kind of control flow analysis that did multiple passes over the body doing each possible narrowing in turn. I've suggested this (and related things) but again, it's not part of the language yet.
For now, I'd say that if you want your code to compile and move on, you will need to use a type assertion to just tell the compiler that you know what you're doing is safe (and that you're just not going to worry about someone doing crazy Math.random()
things like the code above):
function createMyUnionObject<T extends MyUnion["type"]>(
type: T,
value: Extract<MyUnion, { type: T }>["value"]
) {
return { type, value } as any as Extract<MyUnion, { type: T }>; // assert
}
That should work, and you can use createMyUnionObject()
as you'd like. Okay, hope that helps. Good luck!
Upvotes: 2