TypeDrop

2026-09-16 Challenge

2026-09-16 Medium

Typed Job Queue with Priority Scheduling & Status Tracking

You're building the background job processing core for a SaaS platform. Jobs of different types (email delivery, report generation, data export) are enqueued with a priority level, processed by typed handlers, and their lifecycle — pending → running → succeeded / failed — must be tracked in a strongly-typed status ledger that the compiler fully understands.

Goals

  • Define `Job<K>`, `JobStatus<K>`, `HandlerFn<K>`, and `HandlerRegistry` so the compiler enforces correct payload shapes and lifecycle transitions for every job kind.
  • Implement `enqueue` so the `payload` parameter is automatically narrowed to `JobPayloadMap[K]` at every call site with no manual annotation.
  • Implement `processNext` with priority-then-FIFO scheduling, calling the right handler and updating the ledger — all without any unsafe casts or type assertions.
  • Implement `drain` and `summary` so all jobs are processed sequentially and the final tally covers all four status variants, even those with a count of zero.
challenge.ts
/** All supported job kinds. */
type JobKind = "email" | "report" | "export";

/** Payload shapes keyed by job kind. */
type JobPayloadMap = {
  email:  { to: string; subject: string; body: string };
  report: { reportId: string; format: "pdf" | "csv" | "xlsx" };
  export: { datasetId: string; destination: string; compress: boolean };
};

type Priority = 1 | 2 | 3;

// Job<K> carries the exact payload for kind K — no union bleed-through
type Job<K extends JobKind> = /* TODO */;

// Four-variant discriminated union tracking the full lifecycle
type JobStatus<K extends JobKind> = /* TODO */;

// Mapped type: every JobKind maps to its correctly-typed async handler
type HandlerRegistry = /* TODO */;

class JobQueue {
  constructor(handlers: HandlerRegistry) { /* ... */ }
  enqueue<K extends JobKind>(kind: K, payload: JobPayloadMap[K], priority: Priority): string { /* ... */ }
  async processNext(): Promise<string | null> { /* ... */ }
  getStatus<K extends JobKind>(id: string): JobStatus<K> | undefined { /* ... */ }
  async drain(): Promise<void> { /* ... */ }
  summary(): Record<JobStatus<JobKind>["status"], number> { /* ... */ }
}
Hints (click to reveal)

Hints

  • A mapped type `{ [K in JobKind]: HandlerFn<K> }` gives each key its own correctly-typed handler — much safer than an index signature.
  • Inside `processNext`, iterating over the specific job kinds (e.g. with an `if`/`else if` or a helper) lets TypeScript narrow `job.kind` so the payload flows into the handler without casting.
  • Use an indexed access type on the discriminated union — `JobStatus<JobKind>["status"]` — to derive the four string literals for the `Record` in `summary()` automatically.

Or clone locally

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