TypeDrop

A new TypeScript challenge every day. Sharpen your types.

TypeDrop delivers a fresh TypeScript challenge every day, generated by AI. Pick a challenge, open it in StackBlitz (preferred) or CodeSandbox (or clone it locally), and make the tests pass. No accounts, no setup — just you and the type system.

Learn more on GitHub →

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.

Or clone locally

git clone -b challenge/2026-09-12 https://github.com/niltonheck/typedrop.git