Flatten object with custom keys in Typescript

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

Leave a Comment