1

for example:

interface a {
  name: string 
  nested: {
    lastName: string 
    nested: {
      age: number
    }
  }
}

I want that to become:

interface b {
  name: string 
  lastName: string 
  age: number
}

interface values will be dynamic, so I probably need some kind of recursive solution to this

I only ended up with finite solutions

Elnur
  • 53
  • 6

1 Answers1

6

Here's a solution using mapped and conditional types: FlattenPairs constructs a union of pairs like ['name', string] | ['lastName', string] | ['age', number], then Flatten converts this into an object type. I assumed that the nested properties are not necessarily all named nested (and do not necessarily have predictable property names), so the type Primitive is needed as the base case for the recursion.

type Primitive = string | number | boolean
type FlattenPairs<T> = {[K in keyof T]: T[K] extends Primitive ? [K, T[K]] : FlattenPairs<T[K]>}[keyof T] & [PropertyKey, Primitive]
type Flatten<T> = {[P in FlattenPairs<T> as P[0]]: P[1]}

Example:

interface TestInterface {
  name: string 
  nested: {
    lastName: string 
    nested: {
      age: number
    }
  }
}

// {age: number, lastName: string, name: string}
type Test = Flatten<TestInterface>

Playground Link

The helper type FlattenPairs can be written in a slightly simpler way if the nested property name(s) are known:

type FlattenPairs<T> = {[K in keyof T]: K extends 'nested' ? FlattenPairs<T[K]> : [K, T[K]]}[keyof T]

Playground Link

kaya3
  • 47,440
  • 4
  • 68
  • 97
  • works like a charm thanks. can you explain the `& [PropertyKey, Primitive]` part please, is it just a hacky way to fix "Type instantiation is excessively deep and possibly infinite." typescript error or there are other reasons ? – Elnur Jul 25 '21 at 13:14
  • 1
    Yes, that's just a way to fix that error. – kaya3 Jul 25 '21 at 14:08
  • Does this work for union types like `{ foo: number | { bar: string } }` – DaDo Sep 07 '21 at 15:31
  • @DaDo It doesn't seem to, but you may be able to get something working with a [distributive conditional type](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types). Otherwise I suggest asking a separate question. – kaya3 Sep 07 '21 at 16:27
  • I opened a new question [here](https://stackoverflow.com/questions/69095054/how-to-deep-flatten-a-typescript-interface-with-union-types-and-keep-the-full-ob) – DaDo Sep 07 '21 at 22:18
  • @kaya3 it doesn't work if we change the outer `nested` name to anything else. Is there a fix to this? Seems like it skip the object entirely before flattening the inner `nested` – Boom PT Jan 06 '23 at 09:33
  • @BoomPT There are two versions in the answer, the second assumes the nest property name `nested` is known, the first does not. – kaya3 Jan 06 '23 at 10:58
  • @kaya3 here's the [typescript playground](https://tsplay.dev/mAdqBN) that explains my problem – Boom PT Jan 07 '23 at 17:38
  • 1
    @BoomPT You have a different but related problem. It is not a flaw in this answer. I suggest you keep searching to see if there is an existing Q&A which answers your question, or otherwise ask a new question about it. – kaya3 Jan 07 '23 at 19:51