TS: Composizione tramite enum

Feb 13 2023
In questo articolo, mostrerò una variante della composizione del tipo attraverso la dichiarazione preliminare dei campi tramite enum
In una semplice variante della digitazione tramite enum, il nostro tipo si presenta così: Il vantaggio di questo approccio è che creiamo tipi sulla base di elenchi di campi già descritti, limitando così il campo di possibili errori. In questo senso, gli enum diventano un'astrazione aggiuntiva che consente di pensare non in tipi, ma in un sottoinsieme di campi.

In una semplice variante della digitazione tramite enum, il nostro tipo ha questo aspetto:

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;
// }

Il vantaggio di questo approccio è che creiamo tipi sulla base di elenchi di campi già descritti, limitando così il campo di possibili errori. In questo senso, gli enum diventano un'astrazione aggiuntiva che consente di pensare non in tipi, ma in un sottoinsieme di campi.

Tuttavia, la sfumatura di tale composizione è che non disponiamo di uno strumento per comporre diversi enum in un tipo comune.

L'idea è quella di progettare un tipo di utilità che ci permetta di unire le coppie [enum,type][] in un unico tipo in cui ogni campo avrà il tipo assegnato all'enum e non a un singolo campo.

Per creare un tale tipo di utilità, è necessario comprendere 2 principi importanti:

Il principio di costruzione di tipi di utilità ricorsivi.

Il principio del risparmio intermedio del risultato nei tipi di utilità.

Iniziamo a costruire il nostro tipo con una forma di interazione con esso:

Come abbiamo definito in precedenza, vogliamo essere in grado di unire più enumerazioni e assegnare a ciascuna di esse un tipo

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'algoritmo sarà il seguente: prendiamo il nostro array di voci, lo dividiamo in due entità: la prima coppia e tutte le altre coppie, dopodiché trasformiamo la prima coppia [Enum, T] in Record<Enum,T> e scriviamo questo digita sull'accumulatore, ripeti l'algoritmo finché ci sono coppie nell'elenco.

Ecco un esempio di implementazione:


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
}

Perché è utile?

Il vantaggio di tale utilità di tipo è che possiamo suddividere il tipo risultante in blocchi logici di campi attraverso uno strato di enum

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]
]>