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
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 ofkeyof 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