Home > Back-end >  Param type resolution when combining union and intersection types in TypeScript
Param type resolution when combining union and intersection types in TypeScript

Time:05-13

Here are 3 simple types

type T1 =
  | { letter: 'a'; valueFunc: (prop: number) => void; valueType: number }
  | { letter: 'b'; valueFunc: (prop: string) => void; valueType: string }

type T2 = { base: 'low' }

type T3 = T1 & T2

And 2 simple definitions

const var1: T3 = { letter: 'b', base: 'low', valueFunc: (prop) => {}, valueType: 'empty' }

const var2: T3 = { letter: 'a', base: 'low', valueFunc: (prop) => {}, valueType: 0 }

This works perfectly as expected. TS correctly evaluates the types of prop in the valueFunc. However, if I add another type union to T2, TS is no longer able to resolve prop but it can still resolve valueType.

Modified types

type T1 =
  | { letter: 'a'; valueFunc: (prop: number) => void; valueType: number }
  | { letter: 'b'; valueFunc: (prop: string) => void; valueType: string }

type T2 = { base: 'low' } | {noise: 'high'}

type T3 = T1 & T2

const var1: T3 = { letter: 'b', base: 'low', valueFunc: (prop) => {}, valueType: 'empty' }

const var2: T3 = { letter: 'a', noise: 'high', valueFunc: (prop) => {}, valueType: 0 }

Why is that? What am I missing?

CodePudding user response:

TypeScript works very well with discriminated unions

T1 is discriminated, because each union has letter property, whereas T2 is not. You have two ways to resolve this issue.

First way Just add discriminator to T2, for example:

type T2 = { type: '1', base: 'low' } | { type: '2', noise: 'high' }

Second way Make your T2 union more strict. See this answer for more explanation:

type T1 =
  | { letter: 'a'; valueFunc: (prop: number) => void; }
  | { letter: 'b'; valueFunc: (prop: string) => void; }


type UnionKeys<T> = T extends T ? keyof T : never;

type StrictUnionHelper<T, TAll> =
  T extends any
  ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

type T2 = StrictUnion<{ base: 'low' } | { noise: 'high' }>

type T3 = T1 & T2

const var1: T3 = { letter: 'b', base: 'low', valueFunc: (prop) => { } } // prop is string

const var2: T3 = { letter: 'a', noise: 'high', valueFunc: (prop) => { } } // prop is number

Playground

Rule of thumb: If you have a union where each object is different and has nothing in common - add discriminator.

More explanation

1)

Why we use UnionKeys instead of keyof T

We can use T extends any as well. The main point here it to use conditional typings for distributivity. Why ? Because when you use keyof ({a:1}|{b:2}) you will get never because they don't share common properties. See here.

It means that when you are using:

type UnionKeys<T> = T extends any ? keyof T : never;

keyof T is applied to each element in a union separately and not to whole union only because we have used here T extends any.

In general you should treat T extends any - as turn on distributivity and [T] extends [any] - as check it without distributivity.

P.S. You can check my blog for more interesting examples

  • Related