I am seeing confusing behavior in tsc 3.2.2. I have two types that I expect to be equivalent but are not (according to VSCode intellisense - is there a better way to check?).
First, I have a discriminated union discriminated by a type
key. The idea is that I will look up the proper type via the discriminant and then remove the type
key to get the payload type:
interface A { type: 'a', x: number }
interface B { type: 'b', y: string }
type Request = A | B
I have some helper types. Omit
comes from the TS docs, and Discriminate
takes a discrimated union, the discriminant key, and the value for that key to use in the lookup and produces the matching type from the union:
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
type Discriminate<T, U extends keyof T, V extends T[U]> =
T extends Record<U, V> ? T : never
Now I define a helper to get the Request
variant by type
key:
type RequestType<T extends Request['type']> = Discriminate<Request, 'type', T>
type A2 = RequestType<'a'> // Equals A, good so far
Now I add a helper to get the Request
payload type by type
key:
type RequestPayload<T extends Request['type']> = Omit<RequestType<T>, 'type'>
type APayload = RequestPayload<'a'> // {} - That's not right!
However, if I calculate the payload type more directly, it works:
type APayload2 = Omit<RequestType<'a'>, 'type'> // { x: number } - Correct
What is the difference between APayload
and APayload2
? Is this maybe a bug? I think it's far more likely that I'm missing something. They seem like they should be identical.
If you look at the tooltip for the definition of RequestType
, it's actually a union type:
type RequestType<T extends "a" | "b"> = Discriminate<A, "type", T> | Discriminate<B, "type", T>
When you use Omit
on it, keyof
in the Omit
goes only over the keys that are present in all the members of the union, that is, Omit
only sees the type
key and nothing else, and when you omit it the resulting type comes as empty.
You need to use special version of Omit
to fix it. You need UnionOmit
that "distributes" Omit
over the members of the union then immediately assembles the union back again:
type UnionOmit<T, K> = T extends {} ? Pick<T, Exclude<keyof T, K>> : never;
type RequestPayload<T extends Request['type']> = UnionOmit<RequestType<T>, 'type'>
type APayload = RequestPayload<'a'> // {x: number}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With