TypeDrop
2026-07-07 Challenge
2026-07-07
Hard
Typed Middleware Pipeline with Typed Context Propagation
You're building the request-processing pipeline for an API gateway. Each incoming request flows through a chain of typed middleware layers — authentication, rate-limiting, transformation, and logging — where each layer can enrich a shared typed context object, short-circuit with a typed error response, or pass control to the next handler. The hardest part is ensuring the context type is progressively widened as middleware layers run, and that error variants are exhaustively handled.
Goals
- Define a `MiddlewareTuple` type that statically threads the context type through a heterogeneous tuple of middleware layers, ensuring each layer's output type feeds into the next layer's input type.
- Implement `runPipeline`, which executes middleware layers sequentially, short-circuiting on the first `halt` result and returning a fully-typed `PipelineOutcome`.
- Implement the four concrete middleware factories — `makeAuthMiddleware`, `makeRateLimitMiddleware`, `makeTransformMiddleware`, and `makeLogMiddleware` — each of which narrows or widens the context type by adding their own typed properties.
- Implement `handleOutcome`, which exhaustively matches every `GatewayError` kind in a `PipelineOutcome` and returns a typed `HttpResponse`, with a compile-time error if any error kind is unhandled.
challenge.ts
// Core result type — middleware either continues or halts
type MiddlewareResult<TCtx, EKind extends string> =
| { status: "continue"; ctx: TCtx }
| { status: "halt"; error: GatewayError<EKind> };
// Branded error type with discriminated kind
type GatewayError<K extends string> = { kind: K; message: string; statusCode: number };
// A single middleware layer: takes current context, returns enriched context or error
type Middleware<TIn, TOut, EKind extends string> = (
ctx: TIn
) => Promise<MiddlewareResult<TOut, EKind>>;
// Fully-typed pipeline runner — threads context through each layer in order
declare function runPipeline<
TInitial extends BaseContext,
TFinal extends TInitial,
EKind extends string
>(
initial: TInitial,
pipeline: readonly [...MiddlewareTuple<TInitial, TFinal, EKind>]
): Promise<PipelineOutcome<TFinal, EKind>>;
Hints (click to reveal)
Hints
- For `MiddlewareTuple`, try defining it as a union of fixed-length tuple types (length 1 through 4), where each position constrains its input to be the output type of the previous position — the key is that intermediate context types become free type parameters.
- To brand a `string` as `UserId` without using `as`, write a small construction function whose return type is declared as the branded type — TypeScript will accept the assignment since the function body is the single source of truth.
- For the exhaustive `never` check in `handleOutcome`, add a default branch that calls a helper `function assertNever(x: never): never { throw new Error('Unhandled case') }` — if you forget a `GatewayError` kind, the compiler will flag the argument as not assignable to `never`.
Useful resources
Or clone locally
git clone -b challenge/2026-07-07 https://github.com/niltonheck/typedrop.git