DEV Community

Acid Coder
Acid Coder

Posted on • Edited on

Typescript Get Undiscriminating Union Object Type from Array of Object Literal

Sometimes we don't have the type, but we have the array value, and we want get the element type

const obj = [{size:"big", color:"blue"},{size:"small", color:"red"}]
Enter fullscreen mode Exit fullscreen mode

To get the element type, we can do this, first assert the array as const, then

const colorAndSize = [{size:"big", color:"blue"},{size:"small", color:"red"}] as const

type ColorAndSize = typeof colorAndSize //[{ readonly size: "big"; readonly color: "blue";}, { readonly size: "small"; readonly color: "red";}]

type DeepMutable<T> = { -readonly [P in keyof T]: DeepMutable<T[P]> } // remove readonly modifier

type GetElementType<T extends readonly Record<string, unknown>[]> = DeepMutable<T[number]> // { size: "big"; color: "blue";} | { size: "small"; color: "red";}

const abc:GetElementType<ColorAndSize> = {size:"big", color:"blue"} // ok

const efg:GetElementType<ColorAndSize> = {size:"big", color:"red"} // error
Enter fullscreen mode Exit fullscreen mode

playground

we end up with a discriminating union and this may not be ideal because we cant mixed the types. eg we cannot have big and red.

here is how we compute the undiscriminating union

const colorAndSize = [{size:"big", color:"blue"},{size:"small", color:"red"}] as const

type ColorAndSize = typeof colorAndSize //[{ readonly size: "big"; readonly color: "blue";}, { readonly size: "small"; readonly color: "red";}]

type DeepMutable<T> = { -readonly [P in keyof T]: DeepMutable<T[P]> }; // remove readonly modifier

type GetUndiscriminatingElement <T extends Record<string,unknown>[], U extends T[number]=T[0]>= T extends [infer H, ...infer Rest]?
  Rest extends []?{[key in keyof H]:U[key extends keyof U ? key:never]|H[key]}
  : Rest extends Record<string,unknown>[]?GetUndiscriminatingElement<Rest,{[key in keyof H]:U[key extends keyof U ? key:never]|H[key]}>:"impossible route"
:"impossible route"

type A =  GetUndiscriminatingElement<DeepMutable<ColorAndSize>> // { size: "big" | "small"; color: "blue" | "red";}

const a:A = {size:"big", color:"red"} // ok

Enter fullscreen mode Exit fullscreen mode

playground

limitation: the length of the array cannot exceed 999 because the max depth of TS recursion is only 1000

update: simpler and not bounded by recursion depth, thanks to @captainyossarian

const colorAndSize = [{size:"big", color:"blue"},{size:"small", color:"red"}] as const

type ColorAndSize = typeof colorAndSize //[{ readonly size: "big"; readonly color: "blue";}, { readonly size: "small"; readonly color: "red";}]

type GetUndiscriminatingElement <T extends readonly Record<string,unknown>[]> = 
{ -readonly [P in keyof T]: T[P] } extends [infer H,...infer Rest] 
  ? { -readonly [Key in keyof T[number]]: T[number][Key]} 
  : "you forgot to assert as const"

const A :GetUndiscriminatingElement<ColorAndSize> = {size:"big", color:"red"} // ok
Enter fullscreen mode Exit fullscreen mode

playground

Top comments (2)

Collapse
 
captainyossarian profile image
yossarian

It is possible to simplify it a bit:

type Result = {
  -readonly [Key in keyof ColorAndSize[number]]: ColorAndSize[number][Key]
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tylim88 profile image
Acid Coder

very nice, this look much simpler!

I updated the post and added const assertion guarantee to it