1
type A = {
  a: {
    b: string
    c: string
  }
  x: {
    y: number
    z: number
  }
}

i want to:

type B = {
  b: string
  c: string
  y: number
  z: number
}

... implemented through generic type

type B = Unfold<A>

How to do this?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
lanten
  • 47
  • 4

2 Answers2

2

First, we need to get the values of every key of A (or any generic A-like object):

type GetValues<T extends {[key: string]: any}> = T[keyof T];

This generic let's us pass in an A-like object and produces a union of all the objects at every key of A. For demonstration purposes, let's use it to declare a new type Foo based on A:

type Foo = GetValues<A>;

If we hover over Foo we'll see the type it represents - as described above, the union of all the objects at each key of A.

enter image description here

This is close, but we don't want a union, we want an intersection of all the possible types, per your requirement. Fortunately this post on SO has already solved the issue of converting a union to an intersection.

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never;

Now we have everything we need, we can construct our final generic Unfold<T> which composes those two bits:

type Unfold<T extends {[key: string]: any}> = UnionToIntersection<GetValues<T>>;

Now we can use Unfold<T> to convert any A-like type definition to its "unfolded" version:

type UnfoldedA = Unfold<A>;

const foobar: UnfoldedA = {b: '', c: '', y: 0, z: 0};

All put together.

jered
  • 11,220
  • 2
  • 23
  • 34
  • Unfortunatelly it does not work with one more level of nesting – captain-yossarian from Ukraine Jun 08 '21 at 08:00
  • @captain-yossarian that was not specified in the original question as a requirement. If it were, this could be modified to recursively "unfold" nested objects using conditional types. – jered Jun 08 '21 at 08:05
  • 1
    Sure, no problem. Sometimes this can be an issue. I did not want to say that your solution is not correct. You have my upvote – captain-yossarian from Ukraine Jun 08 '21 at 08:11
  • Yes, I don't need nesting. This answer perfectly solves my problem. I stay at `{...} | {...}`, `UnionToIntersection` helps me a lot. Thanks! – lanten Jun 08 '21 at 08:42
  • Just as a suggestion. Rewrite `GetValues` using distribution for input types defined as unions: `type GetValues = T extends {[key: string]: any} ? T[keyof T] : never`. It still holds the same behavior for non-union types aswell. – aleksxor Jun 08 '21 at 10:28
2

I hope smbd will come up with something more readable ))

type A = {
    a: {
        b: string
        c: string
    }
    x: {
        y: number
        z: number,
        w: {
            u: number
        }
    }
}
type Primitives = string | number | boolean | symbol

/**
 * Get all valid nested pathes of object
 */
type AllProps<Obj, Cache extends Array<Primitives> = []> =
    Obj extends Primitives ? Cache : {
        [Prop in keyof Obj]:
        | [...Cache, Prop] // <------ it should be unionized with recursion call
        | AllProps<Obj[Prop], [...Cache, Prop]>
    }[keyof Obj]

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

/**
 * Iterate through each array of nested keys
 * and get last Prop/Value
 */
type Util<Obj, Props extends ReadonlyArray<Primitives>> =
    Props extends []
    ? Obj
    : Props extends [infer First]
    ? First extends keyof Obj
    ? Obj[First] extends Primitives ? Record<First, Obj[First]> : {}
    : never
    : Props extends [infer Fst, ...infer Tail]
    ? Fst extends keyof Obj
    ? Tail extends string[]
    ? Util<Obj[Fst], Tail>
    : never
    : never
    : never

type Unfold<T> = UnionToIntersection<Util<A, AllProps<T>>>

type Result = Unfold<A>

Playground

Explanation about AllProps and Util utils you can find in my blog and