Home > Mobile >  Flatten an interface
Flatten an interface

Time:06-03

I have a usecase where I want to be able to derive the keys/fields of my type based on generic type.

For example:

These are the container interface

export interface IUser {
    id: BigInt;
    name: string;
    balance: number;
    address: Address;
}
export interface Address {
    street: string;
    city: string;
    zipcode: number;
    tags: string[];
}

I want to be able to define type that does something like

const matcher: Matcher<IUser> = {
  id: 1
}

this is something I could do using the Matcher implemented as below

export type Matcher<T> = {
    [K in keyof T]: (number | string | boolean)
}

However now for the usecase like

const deepMatcher: Matcher<IUser> = {
  id: 1,
  user.address.city: 'San Francisco'
}

How can I update my model (Matcher) to support this use-case?

CodePudding user response:

Disclaimer: These types of problems always have a lot of edge cases. This solution works with your example but might break when we introduce things like optional properties or unions. This solution will also not include paths of objects inside of arrays.

The solution consists of two major parts:

First we need a type which will take a type T and constructs a union of all paths of T.

type AllPaths<T, P extends string = ""> = {
    [K in keyof T]: T[K] extends object 
      ? T[K] extends any[] 
        ? `${P}${K & string}` 
        : AllPaths<T[K], `${P}${K & string}.`> extends infer O 
          ? `${O & string}` | `${P}${K & string}`
          : never 
        : `${P}${K & string}`
}[keyof T]

AllPaths is a recursive type. The progress of each path gets stored in P.

It maps over all properties of T and does some checks. If T[K] is an array or not an object, it simply appends K to the current path and returns it.

T[K] extends object 
  ? T[K] extends any[] 
    ? `${P}${K & string}` 
    : /* ... */
  : `${P}${K & string}`

If T[K] is an object, we can recursively call AllPaths with T[K] as the new T and the current path with the current key again. This time we also append a "." to the path since we know that a nested property will be appended later.

AllPaths<T[K], `${P}${K & string}.`> extends infer O 
  ? `${O & string}` | `${P}${K & string}`
  : never 

The infer O stuff here is a workaround so TypeScript does not complain with Type instantiation is excessively deep and possibly infinite. We return both the result of the recursive call and the current path here, so we can have address and also address.street ... as keys later.

Let's see the result here:

type T0 = AllPaths<IUser>
// type T0 = "id" | "name" | "balance" | "address" | "address.street" | "address.city" | "address.zipcode" | "address.tags"

We now need a type which takes a path and fetches us the correct type for the path.

type PathToType<T, P extends string> = P extends keyof T
  ? T[P]
  : P extends `${infer L}.${infer R}` 
    ? L extends keyof T
      ? PathToType<T[L], R>
      : never
    : never 

type T0 = PathToType<IUser, "address.street">
// type T0 = string

This is a recursive type too. P starts as a whole path this time.

We first check if P is a keyof T. If it is, we can return the type of T[P]. If not, we try to split P into the two literal types L and R which must be divided by a dot. If the left string literal matches a key of T, we can recursively call PathToType with T[L] and the rest of the path.


In the end we use both types inside the Matcher type.

type Matcher<T> = Partial<{
    [K in AllPaths<T>]: PathToType<T, K & string>
}>

We allow any string in AllPaths<T> as a key and fetch the corresponding type of the path with PathToType<T, K & string>.

The result looks like this:

type T0 = Matcher<IUser>
// type T0 = {
//    id?: number | undefined;
//    name?: string | undefined;
//    balance?: number | undefined;
//    address?: Address | undefined;
//    "address.street"?: string | undefined;
//    "address.city"?: string | undefined;
//    "address.zipcode"?: number | undefined;
//    "address.tags"?: string[] | undefined;
//}

Playground

  • Related