Featured image

Series: TypeScript

Il problema Link to heading

È comune, in TypeScript, raggruppare un insieme di tipi aventi una particolare relazione in quelle che vengono definite type map, ovvero interfacce che associano ad ognuno dei tipi nell’insieme una o più chiavi per facilitarne il recupero. Un esempio per tutti è la HTMLElementTagNameMap, la quale associa al nome di ogni elemento del DOM il tipo corrispondente:

interface HTMLElementTagNameMap {
  "a": HTMLAnchorElement;
  "abbr": HTMLElement;
  "address": HTMLElement;
  "area": HTMLAreaElement;
  "article": HTMLElement;
  "aside": HTMLElement;
  "audio": HTMLAudioElement;
  "b": HTMLElement;
  ...
}

Vediamo come poter eseguire l’operazione inversa: dato un tipo che sappiamo essere presente in una type map otteremo tutte le sue chiavi.

La soluzione Link to heading

Andremo a creare una type function semanticamente equivalmente alla seguente:

function InvertTypeMap(TypeMap, Type) {
  type ResultingKeysUnion = never

  for(Key in TypeMap) {
    if(TypeMap[Key] is Type) {
      ResultingKeysUnion = ResultingKeysUnion | Key
    }
  }

  return ResultingKeysUnion
}

In essa iteriamo tutte le chiavi della TypeMap e Key dopo Key valutiamo se TypeMap[Key] è il tipo di cui vogliamo ottenere le chiavi. Solo in caso affermativo la Key viene aggiunta alla union da restituire.

Vediamo quindi come esprimere tale funzione nel type system del linguaggio:

type InvertTypeMap<TypeMap, Type> = {
  [Key in keyof TypeMap]: Type extends TypeMap[Key]
    ? TypeMap[Key] extends Type
      ? Key
      : never
    : never;
}[keyof TypeMap];

Utilizziamo un mapped type per iterare le chiavi di TypeMap, e per ogni Key controlliamo la mutua assegnabilità - che non è la migliore definizione di uguaglianza tra tipi in TypeScript, ma è sufficiente nella maggior parte dei casi - tra TypeMap[Key] e Type: in caso affermativo il tipo corrispondente alla Key viene rimappato nella Key stessa, altrimenti in never. Andiamo infine ad eseguire il lookup con tutte le chiavi possibili, [keyof TypeMap], in modo tale da ottenere la union desiderata. Ricordiamo che il tipo never è l’elemento neutro dell’unione tra tipi: per ogni tipo T vale che T | never è pari a T.

Possiamo vedere InvertTypeMap all’opera in questo playground.