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`).
Useful resources
Or clone locally
git clone -b challenge/2026-08-14 https://github.com/niltonheck/typedrop.git