Emular tipos de orden superior en TypeScript: Enfoques prácticos
TypeScript no soporta de forma nativa los tipos de orden superior (HKT), lo que limita la creación de abstracciones reutilizables sobre constructores de tipo como Array, Set o Promise. Los HKT permiten parametrizar no solo los tipos de elementos, sino también el contenedor en sí, facilitando una tipificación fuerte para patrones funcionales como funtores y mónadas. Sin ellos, terminas duplicando código para operaciones similares a map en cada tipo de dato.
Imagina una tarea típica: implementar map para Set.
type MapSet = <A, B>(f: (a: A) => B, set: Set<A>) => Set<B>;
De forma similar, necesitarías versiones para ReadonlySet, LinkedList, ReadonlyArray. La duplicación es inevitable sin HKT, donde una interfaz podría ser simplemente Mappable<F> con una firma F<A> → F<B>.
TypeScript arroja un error: El tipo ‘F’ no es genérico, porque no puede distinguir entre tipos de orden ( para string y → * para Array).
Emulación mediante diccionario de constructores
La solución más sencilla es un diccionario que mapea claves de cadena a tipos.
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 resuelve como Array<number>. Sobre esta base construimos un tipo genérico:
type Mappable<F extends keyof TypeConstructors<unknown>> = <A, B>(
f: (a: A) => B,
list: Kind<F, A>
) => Kind<F, B>;
Implementaciones para tipos específicos:
const mapArray: Mappable<'Array'> = (f, list) => list.map(f);
const mapSet: Mappable<'Set'> = (f, list) => new Set(Array.from(list, f));
Limitaciones de aridad y soluciones
Este enfoque funciona bien para constructores unarios ( → ). Para binarios como Map<K, V>, necesitas un diccionario separado:
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];
Esto complica el manejo de tipos de múltiples aridades. En proyectos de producción, los desarrolladores dependen de bibliotecas:
- hkt-toolbelt: Soporta hasta 4 parámetros, incluye utilidades para funtores y mónadas.
- hkt-core: Se enfoca en rendimiento de tipos, superficie de API mínima.
Estas herramientas ofrecen inferencia de tipos cercana a la nativa, eliminando la necesidad de gestionar manualmente diccionarios.
Principales desafíos en adoptar HKT
A pesar de las constantes solicitudes (issue #1213, #55280), los HKT siguen ausentes en 2026. Las principales barreras son:
- Complejidad del compilador: Introducir polimorfismo de orden, extender la inferencia de tipos y adaptarse al tipado estructural.
- Rendimiento: Tiempos de compilación aumentados en proyectos grandes de programación funcional.
- Compromisos de diseño: Equilibrar expresividad con compatibilidad con JavaScript y evitar sobrecarga sintáctica.
Se han propuesto soluciones como:
- Un marcador especial como
F<~>para constructores de tipo. - Mejorar los tipos condicionales sin añadir nueva sintaxis.
- Emulación pragmática usando características existentes de TypeScript.
Lo que realmente importa
- Los HKT se emulan mediante mapeos de Kind, preservando la tipificación estricta de contenedores.
- Bibliotecas como hkt-toolbelt y hkt-core simplifican la integración en proyectos intermedios y avanzados.
- La aridad requiere diccionarios separados; los tipos unarios son ideales para casos fundamentales.
- El soporte nativo futuro podría llegar mediante mejoras incrementales que no comprometan el rendimiento.
- La emulación permite implementaciones limpias de
map,flatMapy funciones similares sin duplicar código entreArray,Set,Promisey más.
— Editorial Team
Aún no hay comentarios.