Advanced Patterns in Practice

Advanced TypeScript — Advanced Patterns in Practice
Advanced TypeScript
Course 4 · Chapter 9 · Advanced Patterns in Practice

🏛️ Advanced Patterns in Practice

Eight chapters built individual tools: conditional types, recursion, tuple manipulation, reusable guards, brands, template-literal computation, module augmentation, and an eye for the type checker's own performance. This final chapter combines several of them into one working example — a small, genuinely type-safe router — to show what "advanced TypeScript" actually looks like assembled into something real.

What Each Chapter Contributes

Ch.1 & Ch.6 — Template Literals + Conditionals

Extracting typed params directly from a route string, the way ExtractParams did for "/users/:id".

Ch.2 & Ch.3 — Recursion + Tuples

Accumulating a growing map of registered routes across chained .get() calls, the same accumulator pattern as Repeat and Curried.

Ch.4 — Type Guards

Validating an incoming request body against a route's expected shape before a handler ever runs.

Ch.5 — Branded Types

A ValidatedRequest brand marking that validation genuinely happened, not just that a type says it did.

🛣️ A Type-Safe Router DSL

The goal: router.get("/users/:id", handler) should give handler a params object typed as { id: string } automatically — derived from the path string itself, not hand-written:

// Chapter 6's path-param extractor, unchanged type ExtractParams<Path extends string> = Path extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param | keyof ExtractParams<Rest>]: string } : Path extends `${string}:${infer Param}` ? { [K in Param]: string } : {}; type Handler<Path extends string> = (req: { params: ExtractParams<Path>; }) => void;

The router itself accumulates every registered route as a type parameter — one more application of the accumulator pattern from Chapter 2's counting tuple and Chapter 6's tracked builder:

The Router Builder

type Routes = Record<string, Handler<any>>; class Router<R extends Routes = {}> { private routes: R; constructor(routes: R) { this.routes = routes; } get<Path extends string>( path: Path, handler: Handler<Path> ): Router<R & Record<Path, Handler<Path>>> { return new Router({ ...this.routes, [path]: handler }); } dispatch<Path extends keyof R & string>(path: Path, params: ExtractParams<Path>) { this.routes[path]({ params }); } } const router = new Router({}) .get("/users/:id", (req) => { console.log(`Fetching user ${req.params.id}`); // ✅ req.params.id: string, inferred }) .get("/posts/:postId/comments/:commentId", (req) => { console.log(req.params.postId, req.params.commentId); // ✅ both inferred }); router.dispatch("/users/:id", { id: "42" }); // ✅ path must be a registered route // router.dispatch("/unknown", {}); // ❌ "/unknown" was never registered with .get()

Adding Body Validation With a Brand

Chapter 4's guards and Chapter 5's brands combine directly: a handler only ever receives a ValidatedBody, never a raw unknown request body:

type Brand<T, B extends string> = T & { readonly __brand: B }; type ValidatedBody<T> = Brand<T, "Validated">; function validate<T>(body: unknown, guard: (v: unknown) => v is T): ValidatedBody<T> { if (!guard(body)) throw new Error("Invalid request body"); return body as ValidatedBody<T>; } function createUser(body: ValidatedBody<{ name: string }>) { console.log(`Creating ${body.name}`); // ✅ guaranteed to have passed the guard } // createUser({ name: "x" }); // ❌ a plain object isn't ValidatedBody — must go through validate()

💻 Coding Challenges

Challenge 1: Add .post() With a Typed Body

Extend Router with a .post<Path, Body>(path, handler) method where handler receives both params (from the path) and a typed body parameter.

Goal: Practice extending an accumulator-based builder with a second, independently-typed dimension.

→ Solution

Challenge 2: Add Route Groups

Write a .group(prefix, callback) method that registers every route inside callback with prefix prepended to its path (e.g. a group with prefix "/api" turns "/users/:id" into "/api/users/:id"), using a template literal type to compute the combined path.

Goal: Practice combining template literal types with the router's existing accumulator pattern.

→ Solution

Challenge 3: Build a Tiny Type-Safe State Machine

Define a state machine type Transition<State, Event> mapping (currentState, event) pairs to allowed next states (e.g. "idle" + "start""running"), and a transition function that only compiles for valid (state, event) combinations.

Goal: Practice encoding a small DSL (valid state transitions) directly in the type system, combining conditional types and template literal keys.

→ Solution

⚠️ Gotcha: Building a Framework Nobody Asked For

Everything in this chapter is real, useful TypeScript — and also exactly the kind of thing libraries like tRPC, Zod, and Express's own typed routers already solve, tested against thousands of real projects and edge cases a bespoke version won't have hit yet. Building a small type-level DSL is a genuinely valuable skill (and sometimes the right call for something truly project-specific), but before shipping a hand-rolled type-safe router to production, ask whether an existing, battle-tested library already does it — the same "is this worth the cost" judgment Chapter 8 asked about performance applies here to maintenance burden instead.

🎓 Course Complete

That closes Advanced TypeScript. The thread running through all nine chapters: the type system is not just a linter for your JavaScript — it's a small, pure, functional language in its own right, one that can compute, recurse, validate, and even refuse to compile incomplete code on purpose. Conditional types and recursion (Ch.1–2) gave that language control flow; tuples and template literals (Ch.3, Ch.6) gave it data structures and string manipulation; guards and brands (Ch.4–5) gave it a way to make runtime truths visible at compile time; module augmentation (Ch.7) extended it past your own code; and Chapter 8's performance awareness kept all of it honest about its real cost. Course 5 (Building a Production TypeScript Application) is sketched and ready whenever you want to put the full stack — Courses 1 through 4 — to work on one real, substantial project.