3

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?

Tushar Shahi
  • 16,452
  • 1
  • 18
  • 39
Lordbalmon
  • 1,634
  • 1
  • 14
  • 31
  • Typescript inference has limitations and the heuristics it uses do not cover all possible scenarios - for complex cases you need type annotations: https://github.com/Microsoft/TypeScript/issues/2264 – lorefnon May 12 '22 at 07:22

1 Answers1

1

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

  • Wow!.. I am playing with it to understand it better.. could you help me understand why we use `UnionKeys` instead of `keyof T`? Also, why use conditional when we know `T extends T` the same with `T extends any`? – Lordbalmon May 12 '22 at 12:50
  • Btw, feel free tp use `T extrnds any` as well as `T extends T`. In this case - it does not matter. We do conditional typing only for triggering distributivity – captain-yossarian from Ukraine May 12 '22 at 13:10
  • I was wondering if this can be simplified to `type StrictUnion = T extends T ? U & Partial> : never` but that does not work for `undefined` types. The best one can do is remove the excessive support types and leave it at `type StrictUnion = T extends T ? T & Partial, never>> : never` – Lordbalmon May 12 '22 at 14:01
  • @Lordbalmon I understand why you want to reduce it, but in TS, reduced version of type is hard to read and maintain. – captain-yossarian from Ukraine May 12 '22 at 14:18
  • Now that you mention it .. I absolutely agree! – Lordbalmon May 13 '22 at 15:02