TypeDrop
2026-05-28 Challenge
2026-05-28
Medium
Typed Middleware Pipeline Builder
You're building the request-processing layer for an internal API gateway. Incoming requests pass through a chain of typed middleware handlers — each one can enrich the context, short-circuit with a typed error, or pass control to the next handler — all with zero `any`.
Goals
- Define the `Middleware<C, Extra>` type alias with a default type parameter and implement the `Pipeline<C>` builder so that `.use()` correctly widens the context type to `C & Extra` on each call.
- Implement `Pipeline.run()` to execute all registered middleware sequentially, short-circuiting on the first `{ ok: false }` result and catching thrown exceptions as `{ kind: 'internal' }` errors.
- Implement the three middleware factories (`authMiddleware`, `roleMiddleware`, `rateLimitMiddleware`) with their exact return types — no `any`, no type assertions.
- Implement `describeError` with an exhaustive `switch` over all five `MiddlewareError` variants, including every variant-specific field in the output string.
challenge.ts
// Key types and main builder signature
type Middleware<C, Extra = Record<string, never>> =
(ctx: C) => Promise<StepResult<C & Extra>>;
type StepResult<C> =
| { ok: true; ctx: C }
| { ok: false; error: MiddlewareError };
type PipelineResult<T> =
| { ok: true; value: T }
| { ok: false; error: MiddlewareError };
// Fluent builder — each .use() widens the context type
class Pipeline<C> {
constructor(initialCtx: C) { /* ... */ }
use<Extra>(step: Middleware<C, Extra>): Pipeline<C & Extra> { /* ... */ }
run<T>(handler: (ctx: C) => Promise<T>): Promise<PipelineResult<T>> { /* ... */ }
}
// Usage:
const result = await new Pipeline(requestCtx)
.use(authMiddleware()) // ctx gains: { auth: { token: string } }
.use(roleMiddleware("admin")) // ctx gains: { user: { role: string } }
.run(async (ctx) => ctx.user.role);
Hints (click to reveal)
Hints
- For the internal step array in `Pipeline`, consider storing steps as `((ctx: unknown) => Promise<StepResult<unknown>>)[]` and casting only at the boundary inside `use()` — this is the one place where you may need to think carefully about structural compatibility.
- The `use<Extra>()` method should construct and return a brand-new `Pipeline` instance (passing the accumulated steps), rather than mutating `this` — this keeps the type widening sound.
- For `describeError`, TypeScript will flag a missing case if you omit any `kind` variant from the `switch`; use a helper `function assertNever(x: never): never { throw new Error(...) }` in the `default` branch to get compile-time exhaustiveness proof.
Useful resources
Or clone locally
git clone -b challenge/2026-05-28 https://github.com/niltonheck/typedrop.git