É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>.
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 :
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 :
- 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,flatMapet fonctions similaires sans duplication de code entreArray,Set,Promiseet autres.
— Editorial Team
Aucun commentaire pour le moment.