TS : Composition par énumérations

Feb 13 2023
Dans cet article, je vais montrer une variante de composition de type à travers la déclaration préalable des champs via des énumérations
Dans une variante simple du typage par enum, notre type ressemble à ceci : L'avantage de cette approche est que nous créons des types sur la base de listes de champs déjà décrites, limitant ainsi le champ d'erreurs possibles. Dans cette veine, les énumérations deviennent une abstraction supplémentaire qui vous permet de penser non pas en types, mais en un sous-ensemble de champs.

Dans une simple variante de saisie via enum, notre type ressemble à ceci :

export enum TypeFields{
  id = 'id',
  title = 'title'
  ...
}
            //type Record<K extends string, T> = { [P in K]: T }
export type Type = Record<TypeFields,string>

// converted to: 
//
// type Type = {
//     id: string;
//     title: string;
// }

L'avantage de cette approche est que l'on crée des types sur la base de listes de champs déjà décrites, limitant ainsi le champ d'erreurs possibles. Dans cette veine, les énumérations deviennent une abstraction supplémentaire qui vous permet de penser non pas en types, mais en un sous-ensemble de champs.

Cependant, la nuance d'une telle composition est que nous n'avons pas d'outil pour composer plusieurs énumérations en un seul type commun.

L'idée est de concevoir un type utilitaire qui nous permettra de fusionner des paires [enum,type][] en un seul type dans lequel chaque champ aura le type attribué à l'énumération et non à un seul champ.

Pour créer un tel type d'utilitaire, vous devez comprendre 2 principes importants :

Le principe de construction d'un type d'utilitaire récursif.

Le principe de l'enregistrement intermédiaire du résultat dans les types d'utilitaires.

Commençons à construire notre type avec une forme d'interaction avec lui :

Comme nous l'avons défini précédemment, nous voulons pouvoir fusionner plusieurs énumérations et donner à chacun d'eux un type

enum A {
    a = 'a'
}

enum B {
    b = 'b'
}

enum C {
    c = 'c'
}

type MergedType = MergeEnums<[
    [A,string],
    [B,boolean],
    [C,number],
]>

// expected result of MergeEnums utility type: 
//
// type MergedType{
//   [A.a]: string,
//   [B.b]: boolean,
//   [C.c]: number,
// }
export type MergeEnums<Entries extends [keyof any,unknown][]>

L'algorithme sera le suivant - nous prenons notre tableau d'entrées, le divisons en deux entités - la première paire et toutes les autres paires, après quoi nous transformons la première paire [Enum, T] en Record<Enum,T> et écrivons ceci tapez à l'accumulateur, répétez l'algorithme tant qu'il y a des paires sur la liste.

Voici un exemple d'implémentation :


type MergeEnums<
  Entries extends [keyof any,unknown][], // enries array to process
  Acc extends object = {} // accumulator 
> = 
    Entries extends [infer CurrentEntry, ...infer ShiftedEntries] // shift first pair from entries array
    ? ShiftedEntries extends [keyof any, unknown][] // type compare check
    ? CurrentEntry extends [infer Enum,infer Type] // get enum and type for it from shifted entries pair
    ? Enum extends keyof any // type compare check
    ? MergeEnums<ShiftedEntries, Acc & Record<Enum, Type>> // new iteration of MergeEnums with updated accumulator
    // recursion exit
    : Acc 
    : Acc
    : Acc
    : Acc
enum A {
    a = 'a'
}

enum B {
    b = 'b'
}

enum C {
    c = 'c'
}

type Test = MergeEnums<[
    [A,string],
    [B,boolean],
    [C,number]
]>

const test: Test = {
    [A.a]: 'true',
    [B.b]: false,
    [C.c]: 1
}

Pourquoi est-ce utile ?

L'avantage d'un tel utilitaire de type est que nous pouvons diviser le type résultant en blocs logiques de champs via une couche d'énumérations

enum UserProfileSomeRequiredFields{
    id = 'id',
    ...
}

enum UserProfileSomeOptionalFields{
    status = 'status',
    lang = 'lang', 
    ...
}

enum UserProfileSomeResources {
    somePageResource = 'somePageResource',
    someOtherResource = 'someOtherResource'
    ...
}

type SomeResponse = MergeEnums<[
    [UserProfileSomeRequiredFields,number],
    [UserProfileSomeOptionalFields,string],
    [UserProfileSomeResources,boolean]
]>