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?

1
  • Typescript inference has limitations and the heuristics it uses do not cover all possible scenarios - for complex cases you need type annotations: github.com/Microsoft/TypeScript/issues/2264 Commented May 12, 2022 at 7:22

1 Answer 1

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

Sign up to request clarification or add additional context in comments.

5 Comments

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?
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
I was wondering if this can be simplified to type StrictUnion<T, U = T> = T extends T ? U & Partial<Record<keyof T, never>> : 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, U = T> = T extends T ? T & Partial<Record<Exclude<U extends U ? keyof U : never, keyof T>, never>> : never
@Lordbalmon I understand why you want to reduce it, but in TS, reduced version of type is hard to read and maintain.
Now that you mention it 🤔.. I absolutely agree!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.