The HKT Problem in TypeScript
Understand why generic type constructors are hard.
The HKT Problem in TypeScript is a free TypeScript Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the TypeScript Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Is a Higher-Kinded Type
A higher-kinded type (HKT) abstracts not over a concrete type, but over a type constructor: something like Array or Promise that needs an argument to become a real type.
Array alone is not a type. Array<string> is. Array is a function on types, and HKTs let us be generic over such functions.
type StringArray = Array<string>;
type NumberArray = Array<number>;
// Array by itself is a type constructor, not a typeKinds, Briefly
Just as values have types, type constructors have kinds. A plain type like number has kind *. Array has kind * -> *: give it one type, get a type back. Being generic over these is "higher-kinded".
type ValueLike = number; // kind *
// Array needs one argument: kind * -> *
// Map needs two arguments: kind * -> * -> *The Goal: Functor<F>
In languages with HKT support you can write a single Functor<F> abstraction with one map that works for any container F, whether it is an array, an option, or a promise. One interface, many containers.
interface Wish_Functor<F> {
map<A, B>(fa: F<A>, f: (a: A) => B): F<B>;
}
// F<A> here is the wish - and TypeScript rejects itThe Problem
TypeScript does not support applying a type parameter to an argument. You cannot write F<A> when F is a generic parameter. The compiler errors with "Type F is not generic".
// interface Functor<F> {
// map<A, B>(fa: F<A>, f: (a: A) => B): F<B>;
// }
// Error: Type F is not genericWhy TS Lacks It
TypeScript type parameters are first-order: they range over concrete types, not over type constructors. There is no syntax to say "F is something that takes one type argument". So F<A> is meaningless to the compiler.
// You can pass number, string, User as F...
// but not Array, not Promise (the constructors themselves)A Concrete Pain Point
Without HKTs you must duplicate map for every container: one for arrays, one for options, one for promises. The logic is identical but the type system cannot unify them.
declare function mapArray<A, B>(fa: A[], f: (a: A) => B): B[];
declare function mapOption<A, B>(fa: A | null, f: (a: A) => B): B | null;
// Same shape, but no way to abstract over the containerWhat We Want to Express
We want one signature: given a container of A and a function A -> B, return a container of B, preserving the container type. Array stays array; option stays option.
// Wish: map<F, A, B>(fa: F<A>, f: (a: A) => B): F<B>
// where F is preserved across the callOther Languages
Haskell and Scala have HKTs natively, which is why type classes like Functor, Applicative, and Monad are expressible there. TypeScript needs a workaround to approximate the same power.
// Haskell: class Functor f where fmap :: (a -> b) -> f a -> f b
// TypeScript: needs an encoding trick (next lessons)The Encoding Idea
The standard workaround is defunctionalization: instead of applying F directly, we give each container a string tag (a URI) and keep a registry that maps the tag plus an argument to the concrete type. Type application becomes a lookup.
// Instead of F<A>, we write Kind<F, A> where F is a URI string
// and a registry resolves it to the real typeWhat You Will Build
Over the next lessons you build the fp-ts style HKT encoding step by step: a URItoKind registry, a Kind lookup, and finally a generic Functor whose map works for any registered container.
// Roadmap:
// 1. Type constructors via URI strings
// 2. Kind<F, A> resolution
// 3. Generic Functor + instancesWhy It Is Worth Knowing
You rarely write this encoding yourself, but libraries like fp-ts and Effect rely on it. Understanding the trick lets you read their types, write generic abstractions over containers, and appreciate what the type system can and cannot do.
// Recognizing Kind<F, A> in library code is the payoffQuick Check
Test your understanding of the HKT problem.
Recap
You met the higher-kinded type problem.
- HKTs abstract over type constructors like
Array, not concrete types. - TypeScript parameters are first-order, so
F<A>is illegal. - This blocks a single generic
Functorwith onemap. - The fix is an encoding using URI tags and a registry.
Next: defining type constructors via URI strings.
Frequently asked questions
Is the “The HKT Problem in TypeScript” lesson free?
Yes — the full text of “The HKT Problem in TypeScript” is free to read here on the web, and the TypeScript Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the TypeScript Academy course, upgrade to CoddyKit PRO.
What will I learn in “The HKT Problem in TypeScript”?
Understand why generic type constructors are hard. You practise TypeScript Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start TypeScript Academy?
No prior experience is required. TypeScript Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “The HKT Problem in TypeScript” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this TypeScript Academy lesson?
Yes. Every TypeScript Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- The HKT Problem in TypeScript
- Defining Type Constructors
- The Lightweight HKT Pattern
- Generic Functors and Mappers