Advanced TypeScript
Type-Level Programming
Everything in lesson 24 was about annotating values — telling TypeScript what type a variable holds. Advanced TypeScript goes further: you write logic that runs entirely at the type level, producing new types from existing ones. No runtime code. Pure compile-time computation.
This sounds abstract. It becomes concrete the moment you see what it enables: a function that accepts an object and returns a version of it where every property is wrapped in a Promise — and TypeScript knows the exact shape of the result without you spelling it out.
Before the TypeScript syntax, understand the underlying ideas in runnable JavaScript.
Conditional Types — if/else at the Type Level
A conditional type asks: "does this type extend that type? If yes, produce this — if no, produce that."
// Syntax: T extends U ? TrueType : FalseType
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
type C = IsString<"hello">; // "yes" — string literal extends string
// More useful: extract the element type from an array
type Unwrap<T> = T extends Array<infer Item> ? Item : T;
type D = Unwrap<string[]>; // string
type E = Unwrap<number[]>; // number
type F = Unwrap<string>; // string — not an array, returns T itself
infer — Extract Types From Within Other Types
infer appears inside a conditional type and tells TypeScript: "whatever type fits here, capture it and give it a name."
// Extract the return type of any function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function add(a: number, b: number): number {
return a + b;
}
type AddReturn = ReturnType<typeof add>; // number
// Extract the first argument type
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any
? F
: never;
type First = FirstArg<typeof add>; // number
// Unwrap a Promise
type Awaited<T> = T extends Promise<infer V> ? V : T;
type Resolved = Awaited<Promise<string>>; // string
type NotPromise = Awaited<number>; // number
// Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : never;
type E = ElementType<Student[]>; // Student
Mapped Types — Transform Every Property
A mapped type iterates over the keys of an existing type and produces a new type where each property is transformed:
// Syntax: { [K in keyof T]: Transformation }
// Make all properties optional (this is how Partial<T> is implemented)
type Partial<T> = { [K in keyof T]?: T[K] };
// Make all properties readonly
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// Wrap every value in a Promise
type Promisify<T> = { [K in keyof T]: Promise<T[K]> };
// Make every value nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Keep only properties whose value extends a certain type
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Student {
id: number;
name: string;
marks: number;
subject: string;
}
type PromisifiedStudent = Promisify<Student>;
// { id: Promise<number>; name: Promise<string>; marks: Promise<number>; subject: Promise<string> }
type StringProps = OnlyStrings<Student>;
// { name: string; subject: string }
Template Literal Types — String Manipulation at the Type Level
TypeScript can construct new string types by combining other string types — the same way template literals work at runtime, but at compile time:
type EventName = "click" | "focus" | "blur";
// Prefix every event name with "on"
type Handler = \`on\${Capitalize<EventName>}\`;
// "onClick" | "onFocus" | "onBlur"
// Build getter/setter pairs from property names
type Getters<T> = {
[K in keyof T as \`get\${Capitalize<string & K>}\`]: () => T[K]
};
type Setters<T> = {
[K in keyof T as \`set\${Capitalize<string & K>}\`]: (val: T[K]) => void
};
interface Student { name: string; marks: number; }
type StudentAPI = Getters<Student> & Setters<Student>;
// {
// getName: () => string;
// getMarks: () => number;
// setName: (val: string) => void;
// setMarks: (val: number) => void;
// }
Building Real Utility Types From Scratch
Understanding the primitives lets you read — and write — the type definitions of real libraries. Here is how the most useful utility types are actually implemented:
// How TypeScript implements its own built-ins — nothing magic
// Partial: make all properties optional
type MyPartial<T> = { [K in keyof T]?: T[K] };
// Required: make all properties required (remove ?)
type MyRequired<T> = { [K in keyof T]-?: T[K] }; // -? removes optionality
// Readonly: make all properties readonly
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
// Pick: keep only selected keys
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
// Omit: remove selected keys
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>;
// Exclude: remove types from a union
type MyExclude<T, U> = T extends U ? never : T;
// MyExclude<"a"|"b"|"c", "a"> = "b" | "c"
// Extract: keep only types that extend U
type MyExtract<T, U> = T extends U ? T : never;
// NonNullable: remove null and undefined from a type
type MyNonNullable<T> = T extends null | undefined ? never : T;
// Record: construct an object type
type MyRecord<K extends keyof any, V> = { [P in K]: V };
// ReturnType: extract function return type
type MyReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: never;
// Parameters: extract function parameter types as a tuple
type MyParameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
A Real-World Pattern: Strongly Typed Event Emitter
Bringing everything together: a type-safe event emitter where TypeScript knows exactly which data each event carries — and warns if you emit or listen with the wrong type:
// Define all events and their payloads
interface ExamEvents {
"score:submitted": { student: string; score: number };
"session:closed": { total: number; average: number };
"student:failed": { student: string; score: number };
}
// Generic emitter typed to a specific event map
class TypedEmitter<Events extends Record<string, any>> {
private listeners = new Map<keyof Events, Function[]>();
on<K extends keyof Events>(
event: K,
listener: (data: Events[K]) => void, // TypeScript knows the payload type
): () => void {
if (!this.listeners.has(event)) this.listeners.set(event, []);
this.listeners.get(event)!.push(listener);
return () => this.off(event, listener);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners.get(event)?.forEach((fn) => fn(data));
}
off<K extends keyof Events>(event: K, listener: Function): void {
const fns = this.listeners.get(event) ?? [];
this.listeners.set(
event,
fns.filter((f) => f !== listener),
);
}
}
const emitter = new TypedEmitter<ExamEvents>();
// TypeScript knows 'data' is { student: string; score: number }
emitter.on("score:submitted", (data) => {
console.log(data.student, data.score);
console.log(data.missing); // TS error: property does not exist
});
// TypeScript catches wrong payload type
emitter.emit("score:submitted", { student: "Rahul", score: 82 }); // correct
emitter.emit("score:submitted", { student: "Rahul" }); // TS error: score missing
emitter.emit("unknown:event", {}); // TS error: not in ExamEvents
What You Have Learned
Advanced TypeScript is type-level programming — logic that runs at compile time and produces new types from existing ones.
The key ideas:
- Conditional types (
T extends U ? A : B) — if/else at the type level; distributes over union types automatically infer— captures a type from within another type; powersReturnType,Parameters,Awaited- Mapped types (
{ [K in keyof T]: ... }) — iterate over every property and transform it; howPartial,Readonly,Pick, andOmitare actually implemented - Template literal types (
`on${Capitalize<K>}`) — construct new string types; generate method names, event names, route patterns - Every built-in utility type (
Partial,Required,Pick,Omit,Exclude,Extract,ReturnType,Parameters) is built from these four primitives — nothing is magic - A typed event emitter keyed by an interface gives you compile-time safety on every
.on()and.emit()call — the canonical advanced TypeScript pattern
In the next lesson: Runtime Environments — how the same JavaScript runs differently in a browser, Node.js, Deno, and Bun. Different global objects, different module systems, different APIs, different performance characteristics. Understanding the environment is what separates code that works on your machine from code that works everywhere.