The Lightweight HKT Pattern
Implement the Kind/URI defunctionalization trick.
The Lightweight HKT Pattern is a free TypeScript Academy lesson on CoddyKit — lesson 3 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.
Defunctionalization
The trick behind the encoding has a name: defunctionalization. Instead of a real higher-order type function, we represent each function by a tag and resolve it with a single first-order lookup. The URItoKind registry is that lookup table.
type Kind<F extends URIS, A> = URItoKind<A>[F];
// One lookup replaces true type-function applicationThe Kind Type
Kind<F, A> is the public face of the pattern. Read it as "the constructor F applied to A". It hides the registry indexing behind a clean name.
type A = Kind<"Array", number>; // number[]
type B = Kind<"Option", string>; // string | nullConstraining F to URIS
F must be a registered URI. Constrain it with extends URIS so only valid tags can be used and typos become compile errors.
type Kind<F extends URIS, A> = URItoKind<A>[F];
// Kind<"Arrey", number> -> error, not a valid URIWriting Generic Signatures
Now we can write the previously impossible Functor signature. Replace every F<A> with Kind<F, A> and the compiler accepts it.
interface Functor<F extends URIS> {
map<A, B>(fa: Kind<F, A>, f: (a: A) => B): Kind<F, B>;
}Resolution in Action
When you fix F to a concrete URI, Kind resolves to the real container type. A Functor<"Array"> has a map from A[] to B[].
type ArrayFunctor = Functor<"Array">;
// map: <A, B>(fa: A[], f: (a: A) => B) => B[]Multiple Type Arguments
Some constructors take two arguments, like Either<E, A>. fp-ts extends the pattern with URItoKind2 and Kind2<F, E, A> for kind * -> * -> *. The idea scales by adding more registries.
interface URItoKind2<E, A> {
Either: { left: E } | { right: A };
}
type Kind2<F extends URIS2, E, A> = URItoKind2<E, A>[F];The fp-ts Convention
fp-ts established the convention this whole course follows: a URI string per data type, a URItoKind registry extended by declaration merging, and Kind for resolution. Recognizing it lets you read fp-ts and Effect type signatures.
// In fp-ts you will see exactly:
// declare module "fp-ts/HKT" { interface URItoKind<A> { ... } }An Instance Record
An "instance" of an abstraction is a value implementing it for a specific URI. The instance carries the concrete map. We will write the Array instance next lesson; here is its shape.
declare const arrayFunctor: Functor<"Array">;
const out = arrayFunctor.map([1, 2, 3], n => n * 2); // number[]Why This Is "Lightweight"
It is called the lightweight HKT pattern because it needs no compiler changes, no special syntax, just interfaces and indexed access. The cost is a little ceremony (URIs, registries) in exchange for genuine container-generic code.
type Kind<F extends URIS, A> = URItoKind<A>[F];
// No magic, just a typed lookup tableLimitations
The pattern is verbose, and error messages can be cryptic because they mention the encoding rather than your domain. It also requires every container to be registered. For most apps, you consume libraries that already do this rather than build it yourself.
// Trade-off: real abstraction power vs. encoding ceremonyPutting It Together
The full lightweight HKT recipe: define a URI, register it in URItoKind, define abstractions using Kind<F, A>, and provide instances per URI. That is the entire toolkit behind generic functional libraries in TypeScript.
type Kind<F extends URIS, A> = URItoKind<A>[F];
interface Functor<F extends URIS> {
map<A, B>(fa: Kind<F, A>, f: (a: A) => B): Kind<F, B>;
}Quick Check
Test your understanding of the lightweight HKT pattern.
Recap
You assembled the lightweight HKT pattern.
- Defunctionalization replaces type-function application with a lookup.
Kind<F, A>resolves a URI plus argument to a concrete type.- Generic signatures use
Kindinstead ofF<A>. - It scales to two arguments via
Kind2and is the fp-ts convention.
Next: a generic map with real instances.
Frequently asked questions
Is the “The Lightweight HKT Pattern” lesson free?
Yes — the full text of “The Lightweight HKT Pattern” 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 Lightweight HKT Pattern”?
Implement the Kind/URI defunctionalization trick. 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 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “The Lightweight HKT Pattern” 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