Defining Type Constructors
Encode type constructors as interface lookups.
Defining Type Constructors is a free TypeScript Academy lesson on CoddyKit — lesson 2 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.
Naming Type Constructors
The encoding starts by giving each type constructor a unique string identifier called a URI. The URI is a tag that stands in for the constructor wherever we cannot pass the constructor itself.
type ArrayURI = "Array";
type OptionURI = "Option";
// Each container gets a unique string tagA Registry Interface
We keep a single interface that maps each URI to the concrete type it produces for a given argument A. This interface is the registry. Each key is a URI; each value is the resolved type.
interface URItoKind<A> {
Array: Array<A>;
Option: A | null;
}
// URItoKind<number>["Array"] is number[]Looking Up a Constructor
Because the registry is keyed by URI, applying a constructor becomes an indexed access. To get "Array applied to number", you index the registry.
type ArrayOfNumber = URItoKind<number>["Array"]; // number[]
type OptionOfString = URItoKind<string>["Option"]; // string | nullThe URIS Union
The set of valid constructor tags is just the keys of the registry. keyof URItoKind<unknown> gives the union of all registered URIs, which we name URIS.
type URIS = keyof URItoKind<unknown>; // "Array" | "Option"Declaration Merging Extends the Registry
A powerful TypeScript feature: interfaces with the same name merge. Libraries use this so each module can add its own container to URItoKind without editing the original definition.
interface URItoKind<A> {
Tree: { value: A; children: Array<A> };
}
// Now URIS also includes "Tree"Modular Containers
Thanks to merging, a container defined in one file registers itself globally. Anyone importing it can use its URI in generic abstractions. This is exactly how fp-ts adds Option, Either, Task, and more.
// file a.ts
interface URItoKind<A> { Either: { left: A } | { right: A } }
// file b.ts can now reference the "Either" URIA Phantom Carrier
To pass "which constructor" around as a value-level type, instances carry their URI in a property, often a phantom field. The field never holds a real value; it only records the tag for the type system.
interface HasURI<F extends URIS> {
readonly _URI: F;
}
// _URI records which constructor an instance is forPairing URI With Argument
A constructor application needs two things: the URI F and the argument type A. We will combine them with a lookup so that (F, A) resolves to the concrete type via the registry.
type Apply<F extends URIS, A> = URItoKind<A>[F];
type A = Apply<"Array", number>; // number[]
type B = Apply<"Option", string>; // string | nullWhy Strings, Not Constructors
We use strings because TypeScript can store and compare them as literal types, and index a registry with them. We cannot store Array the constructor as a type parameter, but we can store the literal "Array" and look it up.
type Tag = "Array";
type Resolved = URItoKind<boolean>[Tag]; // boolean[]The Lookup Is the Key Move
This indexed access, URItoKind<A>[F], is the heart of the encoding. It turns an impossible type application F<A> into a legal registry lookup. The next lesson formalizes it as Kind<F, A>.
type Kind<F extends URIS, A> = URItoKind<A>[F];
type X = Kind<"Array", number>; // number[]Recap of the Setup
You now have the pieces: URIs as tags, a URItoKind registry mapping tag plus argument to a concrete type, URIS as the union of tags, and declaration merging to extend it. Indexing the registry simulates type application.
// URItoKind = registry, URIS = valid tags,
// URItoKind<A>[F] = "apply constructor F to A"Quick Check
Test your understanding of the registry encoding.
Recap
You learned to encode type constructors with strings.
- Each constructor gets a unique URI tag.
URItoKind<A>maps tags to concrete types.URISis the union of all tags viakeyof.- Declaration merging lets modules register new containers.
Next: packaging the lookup as the lightweight Kind pattern.
Frequently asked questions
Is the “Defining Type Constructors” lesson free?
Yes — the full text of “Defining Type Constructors” 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 “Defining Type Constructors”?
Encode type constructors as interface lookups. 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 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Defining Type Constructors” 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