Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Different behavior in TypeScript type when behind generic type

Tags:

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.

like image 696
Keith Layne Avatar asked Jan 14 '19 17:01

Keith Layne


1 Answers

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}
like image 59
artem Avatar answered Dec 29 '22 01:12

artem