How can an object type with nested subproperties be flattened?

Here’s a more verbose, but more readable implementation:

// Returns R if T is a function, otherwise returns Fallback
type IsFunction<T, R, Fallback = T> = T extends (...args: any[]) => any ? R : Fallback

// Returns R if T is an object, otherwise returns Fallback
type IsObject<T, R, Fallback = T> = IsFunction<T, Fallback, (T extends object ? R : Fallback)>

// "a.b.c" => "b.c"
type Tail<S> = S extends `${string}.${infer T}`? Tail<T> : S;

// typeof Object.values(T)
type Value<T> = T[keyof T]

// {a: {b: 1, c: 2}} => {"a.b": {b: 1, c: 2}, "a.c": {b: 1, c: 2}}
type FlattenStepOne<T> = {
  [K in keyof T as K extends string ? (IsObject<T[K], `${K}.${keyof T[K] & string}`, K>) : K]: 
    IsObject<T[K], {[key in keyof T[K]]: T[K][key]}>
};

// {"a.b": {b: 1, c: 2}, "a.c": {b: 1, c: 2}} => {"a.b": {b: 1}, "a.c": {c: 2}}
type FlattenStepTwo<T> = {[a in keyof T]:  IsObject<T[a], Value<{[M in keyof T[a] as M extends Tail<a> ? M : never]: T[a][M] }>>}

// {a: {b: 1, c: {d: 1}}} => {"a.b": 1, "a.c": {d: 1}}
type FlattenOneLevel<T> = FlattenStepTwo<FlattenStepOne<T>>

// {a: {b: 1, c: {d: 1}}} => {"a.b": 1, "a.b.c.d": 1}
type Flatten<T> = T extends FlattenOneLevel<T> ? T: Flatten<FlattenOneLevel<T>>

It doesn’t support all edge cases, like optional properties, but it should not be too hard to adapt it to the particular edge cases you encounter.

For example, removing the dots in the key names as in your code:

// "a.b.c" => "abc"
type RemoveDots<S> = S extends `${infer H}.${infer T}`? RemoveDots<`${H}${T}`> : S;

type FlattenWithoutDots<T> = {[K in Flatten<T> as RemoveDots<K>]: Flatten<T>[K]};

Leave a Comment

tech