TypeDrop
2026-03-04 Challenge
2026-03-04
Hard
Typed Plugin Middleware Chain with Typed Error Hierarchy
You're building the request-processing core of an API gateway. Incoming requests pass through a chain of strongly-typed middleware plugins (auth, rate-limiting, transformation, logging). Each plugin can either pass the request downstream, short-circuit with a typed error, or mutate the request context — and the orchestrator must collect per-plugin results, surface a typed error hierarchy, and guarantee exhaustive handling at every exit point.
Goals
- Define the full typed error hierarchy (AuthError, RateLimitError, ValidationError, UpstreamError) as a discriminated union and wire it through Result<T,E> and Plugin.
- Implement runChain to execute plugins sequentially, short-circuit on the first failure, record accurate durationMs per plugin, and mark unexecuted plugins as 'skipped'.
- Implement renderError with an exhaustive switch over all PluginError variants, using a never-assertion in the default branch to guarantee compile-time completeness.
- Implement summariseAudit to aggregate a PluginRecord[] into an AuditSummary — narrowing each record variant to compute counts, total duration, and per-plugin error details.
challenge.ts
// Key types and main function signature
export type RequestId = string & { readonly __brand: "RequestId" };
export type PluginName = string & { readonly __brand: "PluginName" };
export type PluginError =
| { kind: "auth"; reason: string }
| { kind: "rate_limit"; retryAfterMs: number }
| { kind: "validation"; field: string; issue: string }
| { kind: "upstream"; statusCode: number; body: string };
export type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
export interface Plugin {
name: PluginName;
execute(ctx: RequestContext): Promise<Result<RequestContext, PluginError>>;
}
export type ChainResult =
| { outcome: "success"; finalContext: RequestContext; audit: PluginRecord[] }
| { outcome: "failure"; failedPlugin: PluginName; error: PluginError; audit: PluginRecord[] };
export async function runChain(
plugins: Plugin[],
initialContext: RequestContext
): Promise<ChainResult> { /* TODO */ }
Hints (click to reveal)
Hints
- A `never`-assertion helper like `function assertNever(x: never): never { throw new Error(...) }` makes the default branch of your switch exhaustive at compile time.
- For PluginRecord, keep `durationMs` only on the `passed` and `failed` variants — the `skipped` variant has no timing to record, which naturally guides your summariseAudit accumulator.
- In runChain, capture `Date.now()` before and after each `await plugin.execute(ctx)` call; store the delta as `durationMs` before deciding which PluginRecord variant to push.
Useful resources
Or clone locally
git clone -b challenge/2026-03-04 https://github.com/niltonheck/typedrop.git