TypeDrop
2026-09-12 Challenge
2026-09-12
Hard
Typed Middleware Pipeline with Typed Context & Error Propagation
You're building the request-handling core for an internal API gateway. Incoming requests flow through a chain of middleware — authentication, rate-limiting, logging, and transformation — each of which can enrich a shared typed context or short-circuit the pipeline with a structured error. The compiler must catch every invalid context mutation, missing required field, and unhandled error variant.
Goals
- Define the `PipelineError` discriminated union, `Result<T,E>` generic, and the four context intersection types so that each middleware layer receives and returns a precisely typed context.
- Implement `composePipeline` with at least two typed overloads (2-arity and 3-arity) so the compiler infers the correct input and output context types and enforces that each middleware's output extends its input.
- Implement `matchError` using a mapped type over `PipelineError["kind"]` so the compiler rejects calls with missing or extra handler keys.
- Implement the three middleware factories (`createAuthMiddleware`, `createRateLimitMiddleware`, `createValidationMiddleware`) so each returns the correct `Middleware<In, Out>` type and produces the appropriate `PipelineError` variant on failure.
challenge.ts
// Core types at a glance
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
export type Role = "admin" | "editor" | "viewer";
export type PipelineError =
| { kind: "AuthError"; message: string; requiredRoles: Role[] }
| { kind: "RateLimitError"; message: string; retryAfterMs: number }
| { kind: "ValidationError"; message: string; fields: string[] }
| { kind: "UpstreamError"; message: string; statusCode: number; upstreamService: string };
export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
export type RawContext = { requestId: string; path: string; method: HttpMethod };
export type AuthedContext = RawContext & { userId: string; roles: Role[] };
export type RateLimitedContext = AuthedContext & { remainingQuota: number };
export type ValidatedContext = RateLimitedContext & { body: unknown };
// Middleware: async fn that enriches context or short-circuits with an error
export type Middleware<In, Out extends In> =
(ctx: In) => Promise<Result<Out, PipelineError>>;
// composePipeline chains middleware left-to-right, threading typed context
export function composePipeline<In1, Out1 extends In1, Out2 extends Out1>(
m1: Middleware<In1, Out1>,
m2: Middleware<Out1, Out2>
): Middleware<In1, Out2>;
Hints (click to reveal)
Hints
- For `composePipeline`'s implementation body, you can use `Middleware<unknown, unknown>` as the internal representation — the overload signatures do the heavy lifting for callers.
- For `matchError`, index into the `handlers` record with `error.kind` after narrowing — TypeScript will infer the correct handler argument type if your `ErrorHandlers` mapped type uses `Extract<E, { kind: K }>` correctly.
- The `Out extends In` constraint on `Middleware` is what prevents middleware from accidentally removing fields from the context — lean on it when writing the factory functions to confirm your return types are correct.
Useful resources
Or clone locally
git clone -b challenge/2026-09-12 https://github.com/niltonheck/typedrop.git