TypeScript's type system is Turing-complete. That's either terrifying or thrilling. Let's make it thrilling.
Type-level programming means writing types that compute. Types that add numbers. Types that parse strings. Types that traverse trees. Not because you should do this in production (you usually shouldn't), but because understanding the limits of the type system makes you a better TypeScript developer. When you know what's POSSIBLE, you know what's PRACTICAL.
This chapter is the summit. It's the hardest material in the book. If you make it through, everything else in TypeScript will feel easy.
What Type-Level Programming Actually Is
Type-level programming is writing types that transform other types through logic, not just through simple mapping. Instead of "make every property optional," it's "if this type is a string, extract the first character; if it's a number, add one; if it's an array, reverse it."
The key insight: TypeScript's type system has primitives for computation:
- Conditional types for branching (
if/else) - Recursive types for looping
- Template literal types for string manipulation
- Mapped types for object iteration
- Tuples for ordered collections (like arrays at the type level)
inferfor pattern matching and destructuring
With these primitives, you can implement any algorithm that doesn't require side effects.
Type-Level Data Structures
Tuples as Lists
At the type level, tuples are your primary data structure:
type List = [string, number, boolean]; // A list of three elements
type Empty = []; // An empty list
Objects as Dictionaries
type Dict = { name: string; age: number }; // Keys map to types
String Literals as... Strings
type Name = "Alice"; // A string value at the type level
Type-Level Arithmetic
TypeScript doesn't have built-in arithmetic for types. You have to build it yourself using tuples:
// Build a tuple of a specific length
type BuildTuple<N extends number, T extends unknown[] = []> =
T["length"] extends N ? T : BuildTuple<N, [...T, unknown]>;
// Addition: concatenate two tuples
type Add<A extends number, B extends number> =
[...BuildTuple<A>, ...BuildTuple<B>]["length"];
type TwoPlusThree = Add<2, 3>; // 5
type ZeroPlusFive = Add<0, 5>; // 5
How it works:
BuildTuple<N>creates a tuple of lengthN(e.g.,BuildTuple<3>=[unknown, unknown, unknown])Add<A, B>concatenates the tuples forAandBand gets the length of the result- The result is
A + B
Subtraction
type Subtract<A extends number, B extends number> =
BuildTuple<A> extends [...BuildTuple<B>, ...infer Rest] ? Rest["length"] : never;
type FiveMinusTwo = Subtract<5, 2>; // 3
Multiplication
type Multiply<A extends number, B extends number, Result extends unknown[] = []> =
B extends 0
? Result["length"]
: Multiply<A, Subtract<B, 1> extends number ? Subtract<B, 1> : never, [...Result, ...BuildTuple<A>]>;
type ThreeTimesFour = Multiply<3, 4>; // 12
This is slow and hits recursion limits quickly. TypeScript's type recursion is limited to about 50 levels. For practical arithmetic, stick to small numbers.
Type-Level String Manipulation
Template literal types with recursion give you string processing:
// String length (using recursion)
type LengthOfString<S extends string, T extends unknown[] = []> =
S extends `${infer First}${infer Rest}`
? LengthOfString<Rest, [...T, unknown]>
: T["length"];
type Len = LengthOfString<"hello">; // 5
// Reverse a string
type ReverseString<S extends string> =
S extends `${infer First}${infer Rest}`
? `${ReverseString<Rest>}${First}`
: S;
type Reversed = ReverseString<"hello">; // "olleh"
// Check if a string is a palindrome
type IsPalindrome<S extends string> = S extends ReverseString<S> ? true : false;
type A = IsPalindrome<"racecar">; // true
type B = IsPalindrome<"hello">; // false
Type-Level List Operations
Treating tuples as lists, you can implement classic list operations:
// Head: get the first element
type Head<T extends unknown[]> = T extends [infer First, ...unknown[]] ? First : never;
// Tail: get everything except the first element
type Tail<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : [];
// Last: get the last element
type Last<T extends unknown[]> = T extends [...unknown[], infer Last] ? Last : never;
// Length: get the length
type Length<T extends unknown[]> = T["length"];
// Reverse a tuple
type Reverse<T extends unknown[]> =
T extends [infer First, ...infer Rest]
? [...Reverse<Rest>, First]
: [];
type Reversed = Reverse<[1, 2, 3]>; // [3, 2, 1]
// Concatenate two tuples
type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B];
// Map over a tuple
type MapTuple<T extends unknown[]> = {
[K in keyof T]: T[K] extends unknown ? Promise<T[K]> : never;
};
// Filter a tuple (keep only elements that extend U)
type FilterTuple<T extends unknown[], U> =
T extends [infer First, ...infer Rest]
? First extends U
? [First, ...FilterTuple<Rest, U>]
: FilterTuple<Rest, U>
: [];
type OnlyStrings = FilterTuple<[1, "a", 2, "b", 3], string>; // ["a", "b"]
Type-Level Parsing
You can parse simple languages at the type level:
// Parse a JSON-like string into a type
type ParseJSON<T extends string> =
T extends `"${infer Value}"` ? Value :
T extends `${infer Num extends number}` ? Num :
T extends "true" ? true :
T extends "false" ? false :
T extends "null" ? null :
never;
type A = ParseJSON<'"hello"'>; // "hello"
type B = ParseJSON<"42">; // 42
type C = ParseJSON<"true">; // true
Practical Type-Level Programming
Most type-level programming is NOT about arithmetic or string reversal. It's about practical type transformations:
Deep Pick
type DeepPick<T, Path extends string> =
Path extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? { [K in Key]: DeepPick<T[Key], Rest> }
: never
: Path extends keyof T
? { [K in Path]: T[Path] }
: never;
interface Config {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
cert: string;
};
};
}
type Host = DeepPick<Config, "server.host">; // { server: { host: string } }
type Cert = DeepPick<Config, "server.ssl.cert">; // { server: { ssl: { cert: string } } }
Path Types (Type-Safe Object Paths)
type Path<T, Prefix extends string = ""> = {
[K in keyof T]: T[K] extends object
? T[K] extends unknown[]
? `${Prefix & string}${K & string}`
: Path<T[K], `${Prefix & string}${K & string}.`>
: `${Prefix & string}${K & string}`;
}[keyof T];
type ConfigPaths = Path<Config>;
// "server.host" | "server.port" | "server.ssl.enabled" | "server.ssl.cert"
Type-Safe get Function
function get<T, P extends Path<T>>(obj: T, path: P): DeepGet<T, P> {
const keys = (path as string).split(".");
let result: unknown = obj;
for (const key of keys) {
result = (result as Record<string, unknown>)[key];
}
return result as DeepGet<T, P>;
}
type DeepGet<T, P extends string> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? DeepGet<T[Key], Rest>
: never
: P extends keyof T
? T[P]
: never;
const config: Config = {
server: {
host: "localhost",
port: 8080,
ssl: { enabled: true, cert: "..." },
},
};
const host = get(config, "server.host"); // Type: string
const cert = get(config, "server.ssl.cert"); // Type: string
const invalid = get(config, "server.invalid"); // Error!
The Limits of Type-Level Programming
TypeScript's type system has hard limits:
- Recursion depth: ~50 levels. Deeply recursive types will error with "Type instantiation is excessively deep and possibly infinite."
- Union size: Unions with 100,000+ members slow the compiler significantly.
- No side effects: Types can't modify external state, call APIs, or do I/O.
- No runtime access: Type-level computations happen at compile time. You can't use them at runtime.
- Limited arithmetic: Large-number arithmetic hits recursion limits quickly.
The Golden Rule
If a type takes more than a few seconds to evaluate, it's too complex. Type-level programming should make your code safer, not slower to compile. If you're building a type-level SQL parser, you've gone too far.
When Type-Level Programming Is Worth It
| Scenario | Worth It? |
|---|---|
| Type-safe route parameters | ✅ Yes |
| Deep partial/readonly types | ✅ Yes |
| Path-based property access | ✅ Yes |
| Form state types | ✅ Yes |
| Type-safe event emitters | ✅ Yes |
| Type-level arithmetic | ❌ Rarely |
| Type-level string reversal | ❌ Almost never |
| Type-level JSON parser | ❌ No |
The best type-level code is the kind that makes your RUNTIME code safer without making your TYPES incomprehensible.
Try This: Type-Level Programming
- Write a
LengthOfString<S>type that computes the length of a string literal. - Write a
Reverse<T>type that reverses a tuple. - Write a
DeepReadonly<T>that makes all nested properties readonly. - Write a
Path<T>type that generates all dot-notation paths through an object type. - Write a
DeepGet<T, P>type that extracts the type at a specific path in an object type.
Time needed: 30 minutes.
What to notice: How recursive types let you process arbitrarily nested structures. How template literal types with infer let you parse strings character by character. How the combination of these techniques gives you a complete type-level programming toolkit.
The Revelation
You've reached the summit. You now understand type-level programming — the most advanced feature of TypeScript's type system. You can write types that compute, types that parse, types that transform other types through complex logic.
But there's a practical question: how do you SHARE these types across files? How do you organize a TypeScript project so that types flow cleanly between modules? How do you publish types for other people to use?
That's the module system. And it's the next chapter.
In the next chapter: ES modules, declaration files, .d.ts files, module resolution, path aliases, and how to structure a TypeScript project that scales.