5

I want to flat object and transform returned values as a type.

Eg:

const myObject = {
  names: {
    title: 'red',
    subtitle: 'green'
  },
}
const returned = [...Object.values(flatten(myObject))] as const
// returns ['red', 'green']
type Type = typeof returned[number]

returned variable is ['red', 'green'] now

Type should be 'red | 'green', but now is an array of string, because typeof returned is string[]. I want to use this type to type prop for my component:

<Component name="red" /> //is correct, but
<Component name=`something different than "red" or "green"` /> //is incorrect.

flatten function:

type FlattenedObject = { [x: string]: string }

export const flattenDaisyChain = (obj: any): FlattenedObject => {
  const result: FlattenedObject = {}

  const transform = (wrapper: FlattenedObject | string, p?: string) => {
    switch (typeof wrapper) {
      case 'object':
        p = p ? p + '.' : ''
        for (const item in wrapper) {
          transform(wrapper[item], p + item)
        }
        break
      default:
        if (p) {
          result[p] = wrapper
        }
        break
    }
  }

  transform(obj)

  return result
}
yudhiesh
  • 6,383
  • 3
  • 16
  • 49
Bart Krakowski
  • 1,655
  • 2
  • 8
  • 25

1 Answers1

12

First, if you want to have any chance of the compiler realizing that Type is "red" | "green" as opposed to string, we have to make sure that myObjct's type includes these literal types. The easy way to do that is with a const assertion:

const myObject = {
  names: {
    title: 'red',
    subtitle: 'green'
  },
} as const;

Next, your flatten() function has a type that is only barely representable in TypeScript 4.1+. I might write it like this, using an answer to a similar question:

type Flatten<T extends object> = object extends T ? object : {
    [K in keyof T]-?: (x: NonNullable<T[K]> extends infer V ? V extends object ?
        V extends readonly any[] ? Pick<T, K> : Flatten<V> extends infer FV ? ({
            [P in keyof FV as `${Extract<K, string | number>}.${Extract<P, string | number>}`]:
            FV[P] }) : never : Pick<T, K> : never
    ) => void } extends Record<keyof T, (y: infer O) => void> ?
    O extends infer U ? { [K in keyof O]: O[K] } : never : never

const flatten = <T extends object>(obj: T): Flatten<T> => { /* impl */ }

It uses recursive conditional types, template literal types, and key remapping in mapped types to recursively walk down through all the properties and concatenate keys together with . in between. I have no idea if this particular type function will work for all the use cases of flatten(), and I wouldn't dare to pretend that it doesn't have demons lurking in it.


If I use that signature, and call flatten(myObject) with the above const-asserted myObject, you get this:

const flattenedMyObject = flatten(myObject);
/* const flattenedMyObject: {
    readonly "names.title": "red";
    readonly "names.subtitle": "green";
} */

Hooray! And this is enough information for Type to be "red" | "green":

const returned = [...Object.values(flatten(myObject))] as const
type Type = typeof returned[number] // "red" | "green"

Also hooray.


I suppose if you don't really care about tracking the key names, you can simplify the flatten() type signature considerably by returning something with a string index signature:

type DeepValueOf<T> = object extends T ? object :
    T extends object ? DeepValueOf<
        T[Extract<keyof T, T extends readonly any[] ? number : unknown>]
    > : T

const flatten = <T extends object>(obj: T): { [k: string]: DeepValueOf<T> } => {
    /* impl */ }

which produces

const flattenedMyObject = flatten(myObject);
/* const flattenedMyObject: {
   [k: string]: "green" | "red";
 } */

and eventually

const returned = [...Object.values(flatten(myObject))] as const
type Type = typeof returned[number] // "red" | "green"

I'm less worried about DeepValueOf than I am about Flatten, but I would be surprised if there aren't some edge cases. So you'd need to really test before using it in any sort of production code.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360