Keith Layne
Keith Layne

Reputation: 3795

Different behavior in TypeScript type when behind generic type

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.

Upvotes: 5

Views: 492

Answers (1)

artem
artem

Reputation: 51769

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 UnionOmitthat "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}

Upvotes: 4

Related Questions