TypeDrop

2026-08-14 Challenge

2026-08-14 Hard

Typed Middleware Pipeline with Typed Context & Error Boundaries

You're building the request-handling core for an internal API gateway. Every inbound request flows through a chain of middleware — authentication, rate-limiting, validation, transformation — each of which can enrich the shared context object, short-circuit with a typed error, or pass control to the next handler. The pipeline must be fully type-safe: each middleware declares what context properties it *reads* and what it *adds*, the compiler enforces correct ordering, and every short-circuit produces an exhaustively-matchable typed error.

Goals

  • Define the `Result<T,E>` discriminated union and all four `PipelineError` variants so every error kind is exhaustively matchable.
  • Implement the `Pipeline<Ctx, FinalOutput>` class so that `.use()` chains middleware and correctly threads the enriched context type through each step, and `.run()` short-circuits on the first `{ ok: false }` result.
  • Implement `withAuth`, `withRateLimit`, and `withBodyValidation` middleware factories with correct generic signatures — no `any`, no type assertions.
  • Ensure the branded `RequestId` type is created via the `Brand` helper so plain strings are rejected by the compiler wherever `RequestId` is expected.
challenge.ts

// Core types at a glance

declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [__brand]: B };

export type RequestId = Brand<string, "RequestId">;
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

export type Result<T, E> =
  | { ok: true;  value: T }
  | { ok: false; error: E };

export type PipelineError =
  | { kind: "auth";       message: string; statusCode: 401 | 403 }
  | { kind: "rate_limit"; retryAfterMs: number }
  | { kind: "validation"; fields: string[]; message: string }
  | { kind: "upstream";   cause: unknown;  statusCode: number };

// Middleware<In, Out, FinalOutput>
export type Middleware<In, Out, FinalOutput> = (
  ctx: In,
  next: (enriched: Out) => Promise<Result<FinalOutput, PipelineError>>
) => Promise<Result<FinalOutput, PipelineError>>;

// Pipeline — each .use() narrows Ctx to NextCtx
export class Pipeline<Ctx, FinalOutput> {
  use<NextCtx>(mw: Middleware<Ctx, NextCtx, FinalOutput>): Pipeline<NextCtx, FinalOutput> { … }
  run(terminal: (ctx: Ctx) => Promise<Result<FinalOutput, PipelineError>>): Promise<Result<FinalOutput, PipelineError>> { … }
}
Hints (click to reveal)

Hints

  • For `Pipeline.use()`, store the existing chain as a closure: the new pipeline's internal runner calls the middleware with the old runner as `next`, threading context enrichment automatically.
  • The `Middleware<In, Out, FinalOutput>` triple-generic is key — `In` is what you read, `Out` is what you pass to `next`, and `FinalOutput` stays fixed across the whole chain.
  • To avoid `as` in `makeRequestId`, use an object spread trick or a helper function that returns `Brand<string, B>` — remember the brand field only exists at the type level (it's a `unique symbol`).

Or clone locally

git clone -b challenge/2026-08-14 https://github.com/niltonheck/typedrop.git