Home > Software design >  Generic index signature only for specific sub types
Generic index signature only for specific sub types

Time:09-23

Every of our database tables has an identifier (id) and version. Which means every concrete Entity extends from the Entity interface in order to have those two attributes.

interface Entity {
  id: number;
  version: number;
}

Here is a multi-level entity for example purposes.

interface Address extends Entity {
    street: string;
    hno: string;
    zip: string;
    city: string;
    country: string;
}

interface Department extends Entity {
    name: string;
}

interface User extends Entity {
    name: string;
    birthday: string;
    address: Address;
    department: Department;
}

Now I want a generic type which replaces all appearances of id and version (even the fields in the sub-objects) into optional. Which means from

User {
    dbid: number;
    version: number;
    name: string;
    birthday: string;
    address: {
        dbid: number;
        version: number;
        street: string;
        hno: string;
        zip: string;
        city: string;
        country: string;
    };
    department: {
        dbid: number;
        version: number;
        name: string;
    };
}

into

User {
    dbid?: number;
    version?: number;
    name: string;
    birthday: string;
    address: {
        dbid?: number;
        version?: number;
        street: string;
        hno: string;
        zip: string;
        city: string;
        country: string;
    };
    department: {
        dbid?: number;
        version?: number;
        name: string;
    };
}

As I said as a generic type, because I have a lot of different entities where I want to perform the same.

Something like

export type NewEntity<T extends Entity> = {
  // remove id and version fields
  [K in keyof Omit<T, 'dbid' | 'version'>]: T[K] extends Entity ? NewEntity<T[K]> : T[K];
};

but this removes only the two attributes but lacks the redefinition of

id?: number;
version?: number

CodePudding user response:

You can define a recursive generic type like this one:

type NewEntity<T extends Entity> = ({
  [K in Exclude<keyof T, keyof Entity>]: 
    T[K] extends Entity 
      ? NewEntity<T[K]> 
      : T[K]
} & {
  [K in Extract<keyof T, keyof Entity>]?: T[K]
}) extends infer O ? { [K in keyof O]: O[K] } : never

It creates an intersection of two mapped types.

The first one is including all properties which are not in Entity (id and version). If a property type T[K] extends Entity, we can call the type NewEntity recursively with it.

The second type includes the properties of T which are also in Entity. Those are made optional.

The extends infer O ? { [K in keyof O]: O[K] } : never is only for aesthetic reasons and makes the resulting type readable. (See this for more information)


Here is the result:

type NewUser = NewEntity<User>

// type NewUser = {
//     name: string;
//     birthday: string;
//     address: {
//         street: string;
//         hno: string;
//         zip: string;
//         city: string;
//         country: string;
//         id?: number | undefined;
//         version?: number | undefined;
//     };
//     department: {
//         name: string;
//         id?: number | undefined;
//         version?: number | undefined;
//     };
//     id?: number | undefined;
//     version?: number | undefined;
// }

Playground

  • Related