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