Retour à l'accueil

HKT en TypeScript : émulation et pratique

L'article couvre l'émulation des Types de rang supérieur en TypeScript via les mappages Kind et les bibliothèques. Décrit les limitations d'arité, les implémentations de map pour les conteneurs, et les obstacles au support natif. Utile pour créer des abstractions FP type-sûres.

Émulation HKT en TypeScript : Kind et bibliothèques
Advertisement 728x90

Émuler les types de haut niveau en TypeScript : des approches pratiques

TypeScript ne prend pas en charge nativement les types de haut niveau (HKT), ce qui limite la création d'abstractions réutilisables sur des constructeurs de type comme Array, Set ou Promise. Les HKT permettent de paramétrer non seulement les types d'éléments, mais aussi le conteneur lui-même, offrant une typage rigoureux pour des patterns fonctionnels tels que les foncteurs et les monades. Sans eux, on est obligé de dupliquer du code pour les opérations map-like sur chaque type de données.

Prenons une tâche classique : implémenter map pour Set.

type MapSet = <A, B>(f: (a: A) => B, set: Set<A>) => Set<B>;

De même, il faudrait des versions pour ReadonlySet, LinkedList, ReadonlyArray. La duplication est inévitable sans HKT, où une interface pourrait simplement être Mappable<F> avec une signature F<A> → F<B>.

Google AdInline article slot

TypeScript lance une erreur : Le type ‘F’ n’est pas générique, car il ne peut pas distinguer les genres de type ( pour string et → * pour Array).

Émulation via dictionnaire de constructeurs

La solution la plus simple consiste à utiliser un dictionnaire associant des clés chaînes aux types.

interface TypeConstructors<A> {
  'Array': Array<A>;
  'LinkedList': LinkedList<A>;
  'ReadonlySet': ReadonlySet<A>;
  'Set': Set<A>;
}

type Kind<F extends keyof TypeConstructors<unknown>, A> = TypeConstructors<A>[F];

Kind<'Array', number> se résout en Array<number>. Sur cette base, on construit un type générique :

Google AdInline article slot
type Mappable<F extends keyof TypeConstructors<unknown>> = <A, B>(
  f: (a: A) => B,
  list: Kind<F, A>
) => Kind<F, B>;

Implémentations pour des types spécifiques :

const mapArray: Mappable<'Array'> = (f, list) => list.map(f);
const mapSet: Mappable<'Set'> = (f, list) => new Set(Array.from(list, f));

Limites d'arité et solutions

Cette approche fonctionne bien pour les constructeurs unaires (). Pour les binaires comme Map<K, V>, il faut un dictionnaire séparé :

interface TypeConstructors2<A, B> {
  Map: Map<A, B>;
  Record: Record<A, B>;
}

type Kind2<F extends keyof TypeConstructors2<unknown, unknown>, A, B> = TypeConstructors2<A, B>[F];

Cela complique la gestion des types multi-arguments. Dans les projets réels, les développeurs s'appuient sur des bibliothèques :

Google AdInline article slot
  • hkt-toolbelt : prend en charge jusqu’à 4 paramètres, inclut des utilitaires pour foncteurs et monades.
  • hkt-core : axé sur les performances de type, surface API minimale.

Ces outils offrent une inférence de type proche de celle native des HKT, éliminant la nécessité de gérer manuellement les dictionnaires.

Principaux défis d'adoption des HKT

Malgré les demandes répétées (problème #1213, #55280), les HKT restent absents en 2026. Les obstacles majeurs sont :

  • Complexité du compilateur : introduction de polymorphisme de genre, extension de l'inférence de type, adaptation au typage structurel.
  • Performance : temps de compilation augmentés dans les grands projets fonctionnels.
  • Choix de conception : équilibre entre expressivité et compatibilité JavaScript, évitement de la surcharge syntaxique.

Des solutions proposées incluent :

  • Un marqueur spécial comme F<~> pour les constructeurs de type.
  • Amélioration des types conditionnels sans nouvelle syntaxe.
  • Émulation pragmatique utilisant les fonctionnalités existantes de TypeScript.

Ce qui compte vraiment

  • Les HKT sont émulés via des mappings Kind, préservant un typage strict pour les conteneurs.
  • Des bibliothèques comme hkt-toolbelt et hkt-core simplifient l’intégration dans les projets intermédiaires à avancés.
  • L’arité nécessite des dictionnaires distincts ; les types unaires sont idéaux pour les cas fondamentaux.
  • Un support natif futur pourrait arriver par des améliorations progressives sans compromettre les performances.
  • L’émulation permet des implémentations propres de map, flatMap et fonctions similaires sans duplication de code entre Array, Set, Promise et autres.

— Editorial Team

Advertisement 728x90

Lire ensuite