3

Let's say we have a type with some nested properties, like so:

type Deep = {
    foo: number;
    bar: {
        nested1: string;
        nested2: number[];
    }
}

But we need to pass objects of that type through a system which can't handle nested properties. So we'll flatten the objects, using a separator like _ to denote nested properties:

type Flat = {
    foo: number;
    bar_nested1: string;
    bar_nested2: number[];
}

We can implement functions for flattening and unflattening these objects. But, if we want the functions to be generic, how do we represent their return types?. Is it even possible in Typescript currently (v4.5)?

function flatten<TDeep>(deep: TDeep): Flatten<TDeep>; // How do we define `Flatten<>`?
function unflatten<TFlat>(flat: TFlat): Unflatten<TFlat>; // How do we define `Unflatten<>`?

I had a stab at Unflatten<> using Typescript 4.5.

type Unflatten<T> = WithoutDeepProps<T> & WithUnflattenedProps<T>

type WithoutDeepProps<T> = {
    [Property in keyof T as Exclude<Property, `${string}_${string}`>]: T[Property];
}

type WithUnflattenedProps<T> = {
    [Property in keyof T as ParentOf<Property>]: {
        [ChildProperty in ChildOf<Property>]: T[Property]
    }
}

type ParentOf<T> = T extends `${infer Parent}_${string}` ? Parent : never;
type ChildOf<T> = T extends `${string}_${infer Child}` ? Child : never;

This kind-of works but it unions the nested property types. So, using Flat from above:

// Unflatten<Flat> gives:
foo: number;
bar: {
    nested1: string | number[];
    nested2: string | number[];
}

I haven't even tried Flatten<> because I don't know how I would convert one property (e.g. bar) into many (bar_nested1, bar_nested2)

Tim S
  • 689
  • 6
  • 16
  • 1
    When flattening, you are going to run into the issue of potential key duplication (e.g. the combination of parent_child key already exists as a sibling of parent). – jsejcksn Dec 10 '21 at 17:54
  • Likewise, unflatten has to assume no underscores in property names to work reliably. – Ingo Bürk Dec 10 '21 at 18:06
  • There's all kinds of caveats with such flattening (see https://stackoverflow.com/a/66620803/2887218 and https://stackoverflow.com/a/69111325/2887218 and https://stackoverflow.com/a/65923825/2887218) since it's not clear what you expect to see with index signatures, unions, optional properties, etc etc etc. – jcalz Dec 10 '21 at 19:10

1 Answers1

1

The first part would be to understand why you get that union there. It appears that when mapping, if the as clause produces duplicate properties (as it would here, as bar_nested1 and bar_nested1 end up being mapped to the same property bar) typescript will call the property type expression of the mapped type not with each individual key, but rather with their union. (So Property will be bar_nested1 | bar_nested1). So when you index T[Property] you get a union of both types.

We can demonstrate this using this type:


type WithUnflattenedProps<T> = {
    [Property in keyof T as ParentOf<Property>]: [Property]
}

type Y = Id<WithUnflattenedProps<Flat>>
// Will be 
// type Y = {
//     bar: ["bar_nested1" | "bar_nested2"];
// }

Playground Link

We can fix this by rebuilding each property based on ChildProperty instead of using Property:

type WithUnflattenedProps<T> = {
    [Property in keyof T as ParentOf<Property>]: {
      [ChildProperty in ChildOf<Property>]:  T[`${ParentOf<Property>}_${ChildProperty }` & keyof T]
    }
}

Playground Link

You could also apply the type recursively to get unflatten a deeper hierarchy Playground Link

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357