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.