The type-level toolkit — literal and template literal types, mapped types with key remapping, conditional types with infer, and recursive types for JSON and trees.
Why: literal types pin a value exactly; template literal types build string patterns from pieces, so malformed strings — wrong HTTP method, missing slash — fail at compile time.
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
// Template literal types compose string patterns
type Endpoint = `/${string}`;
type Route = `${Method} ${Endpoint}`;
const ok: Route = 'GET /users'; // ok
// const bad1: Route = 'FETCH /users'; // Error: FETCH is not a Method
// const bad2: Route = 'GET users'; // Error: missing the leading /
// Generate whole unions mechanically
type Entity = 'user' | 'post';
type EventName = `${Entity}:${'created' | 'deleted'}`;
// 'user:created' | 'user:deleted' | 'post:created' | 'post:deleted'
function emit(event: EventName) {
console.log('emit', event);
}
emit('post:created'); // ok
// emit('post:liked'); // Error
console.log(ok);Why: a mapped type loops over the keys of an existing type and transforms each property — this is literally how Partial, Readonly, and Record are implemented. Key remapping with "as" can even rename the keys.
type User = { id: number; name: string; email: string };
// Transform every property
type Nullable<T> = { [K in keyof T]: T[K] | null };
type DraftUser = Nullable<User>;
// { id: number | null; name: string | null; email: string | null }
// Key remapping with 'as' — rename keys while mapping
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string }
// Modifiers: -readonly removes readonly, ? adds optional
type Editable<T> = { -readonly [K in keyof T]?: T[K] };
const draft: DraftUser = { id: null, name: 'Alice', email: null };
console.log(draft);Why: T extends U ? A : B is an if/else at the type level. infer captures part of the matched type, and conditionals distribute over unions — together they power most advanced type utilities.
// if/else for types
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<'hello'>; // 'yes'
type B = IsString<42>; // 'no'
// 'infer' captures a piece of the matched type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Num = ElementOf<number[]>; // number
type Str = ElementOf<string[]>; // string
// Conditionals distribute across union members
type NonString<T> = T extends string ? never : T;
type Mixed = NonString<string | number | boolean>; // number | boolean
// Practical: unwrap a promise, pass anything else through
type Unwrap<T> = T extends Promise<infer V> ? V : T;
type R1 = Unwrap<Promise<number>>; // number
type R2 = Unwrap<string>; // string
// Verify your types with assignments the compiler must accept:
const check1: R1 = 123;
const check2: Mixed = false;
console.log(check1, check2);Why: a type that refers to itself models nested data of any depth — JSON payloads, file trees, comment threads. The compiler follows the recursion as deep as your data goes.
// JSON: every valid JSON value, at any nesting depth
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json };
const payload: Json = {
user: { name: 'Alice', tags: ['admin', 'dev'] },
active: true,
meta: null,
};
// A generic tree
type TreeNode<T> = {
value: T;
children: TreeNode<T>[];
};
const tree: TreeNode<string> = {
value: 'root',
children: [
{ value: 'a', children: [] },
{ value: 'b', children: [{ value: 'b1', children: [] }] },
],
};
console.log(payload, tree.children[1].children[0].value); // … 'b1'