Typescript WTF Moment 12: Beware of Object Literal Unions

Acid Coder
2 min readJun 20, 2023
type ABC = { A: number, B: number } | { C: number }
const abc: ABC = { A: 1, B: 2, C: 3 } // no error!

playground

Typescript excess property check allow any property from unions of object literal

Solutions

Assign type `never` to excess properties and make them optional

type ABC = { A: number, B: number, C?: never } | { A?: never, B?: never, C: number }
const abc: ABC = { A: 1, B: 2, C: 3 } // error!
const ab: ABC = { A: 1, B: 2 } // ok!
const c: ABC = { C: 3 } // ok!
const bc: ABC = { B: 2, C: 3 } // error!

playground

With common properties that have different types, Typescript is able to discriminate unions

However it is a lot of work if we have a large unions of object literal

We need to create an utility type that automate this process

type GetAllKeys<T> = T extends T ? keyof T : never
type CreateOptionalExcessProperty<T, Y> = T extends T ? T & Partial<Record<Exclude<GetAllKeys<Y>, keyof T>, never>> : never
type StrictUnion<T> = CreateOptionalExcessProperty<T, T>
type ABC = StrictUnion<{ A: number, B: number } | { C: number }>;
const abc: ABC = { A: 1, B: 2, C: 3 } // error!
const ab: ABC = { A: 1, B: 2 } // ok!
const c: ABC = { C: 3 } // ok!
const bc: ABC = { B: 2, C: 3 } // error!

playground

The purpose of `T extends T` is to distribute unions

credit

--

--

Acid Coder

Typescript Zombie. Youtube Pikachu On Acid. (Unrelated to programming but by watching it you become a good developer overnight)