TypeDrop

2026-10-22 Challenge

2026-10-22 Hard

Typed Middleware Pipeline with Context Propagation

You're building the request-processing backbone for an HTTP gateway. Middleware handlers are composed into a typed pipeline where each layer can read from and write to a shared context object — the compiler must guarantee that every middleware declares exactly what context keys it reads (its dependencies) and writes (its contributions), that contributions from earlier layers are available to later ones, and that the final handler only executes when all required keys are present in the accumulated context.

Goals

  • Implement `Spread<A, B>`, `AccumulateContext<Middlewares, Ctx>`, and `VerifyReads<Reads, Ctx>` as pure mapped/conditional types with no `any`.
  • Implement the `Pipeline` class so that `.use()` rejects (via `never`) any middleware whose `Reads` keys are not yet present in `CurrentCtx`, and accumulates `Writes` into the next `CurrentCtx`.
  • Implement the three middleware factories (`makeAuthMiddleware`, `makeLoggingMiddleware`, `makeRateLimitMiddleware`) and the `makeEchoHandler` with correct `Reads`/`Writes` declarations and runtime logic.
  • Assemble `buildGatewayPipeline` in the correct order (auth → logging → rateLimit → handler) and verify that swapping any two layers produces a compile-time type error.
challenge.ts
// Key types and main Pipeline class signature

export type Spread<A extends object, B extends object> = {
  [K in keyof A | keyof B]: K extends keyof B ? B[K] : K extends keyof A ? A[K] : never;
};

export type MiddlewareDescriptor<Reads extends object, Writes extends object> = {
  reads: Reads;
  writes: Writes;
};

export type AccumulateContext<
  Middlewares extends MiddlewareDescriptor<object, object>[],
  Ctx extends object = {}
> = /* recursive — walks the tuple, spreading each `writes` into Ctx */ Ctx;

export type VerifyReads<Reads extends object, Ctx extends object> =
  keyof Reads extends keyof Ctx ? true : false;

export type MiddlewareFn<Ctx extends object, Contribution extends object> = (
  req: RawRequest,
  ctx: Ctx,
  next: (contribution: Contribution) => Promise<PipelineResponse>
) => Promise<PipelineResponse>;

export class Pipeline<
  Middlewares extends MiddlewareDescriptor<object, object>[] = [],
  CurrentCtx extends object = {}
> {
  use<Reads extends object, Writes extends object>(
    fn: VerifyReads<Reads, CurrentCtx> extends true ? MiddlewareFn<Reads, Writes> : never
  ): Pipeline<[...Middlewares, MiddlewareDescriptor<Reads, Writes>], Spread<CurrentCtx, Writes>>;

  handle<Reads extends object>(
    handler: VerifyReads<Reads, CurrentCtx> extends true ? FinalHandler<Reads> : never
  ): ExecutablePipeline;
}
Hints (click to reveal)

Hints

  • For `AccumulateContext`, use `Middlewares extends [infer Head, ...infer Tail]` to peel off one descriptor at a time and `Spread` its `writes` into the growing `Ctx` — then recurse on `Tail`.
  • The `never` trick in `.use()` works because passing a real function where `never` is expected is a type error — no extra overloads needed; the conditional on `VerifyReads` does all the work.
  • Keep `Pipeline`'s runtime internals typed as `MiddlewareFn<object, object>[]` and cast with `as` only inside the class body — all the safety lives in the public API's generic signatures.

Or clone locally

git clone -b challenge/2026-10-22 https://github.com/niltonheck/typedrop.git