TypeDrop

2026-03-21 Challenge

2026-03-21 Easy

Typed Expense Report Aggregator

You're building the expense reporting module for a small business finance tool. Employees submit raw expense entries from a form, and your engine must validate them, tag each with a derived reimbursement status, and produce a grouped, fully typed summary — with zero `any`.

Goals

  • Implement `validateExpenseEntry` to narrow all `unknown` fields into a fully-typed `ExpenseEntry`, returning the first `ValidationError` encountered.
  • Implement `deriveStatus` to map an `ExpenseEntry`'s amount to the correct `ReimbursementStatus` discriminated union branch.
  • Implement `buildExpenseReport` to validate, tag, sort, and aggregate raw entries into a complete `ExpenseReport` — collecting both valid tagged entries and rejected ones.
  • Produce correct `CategorySummary` objects with per-status counts using `Record<ReimbursementStatus["kind"], number>` — with no `any`.
challenge.ts

// Key types & main function signature

type ExpenseCategory = "travel" | "meals" | "software" | "equipment" | "other";

type ReimbursementStatus =
  | { readonly kind: "auto_approved"; readonly approvedAt: Date }
  | { readonly kind: "needs_review"; readonly reason: string }
  | { readonly kind: "flagged"; readonly reason: string; readonly flaggedAmount: number };

type Result<T, E> =
  | { readonly ok: true;  readonly value: T }
  | { readonly ok: false; readonly error: E };

interface TaggedExpenseEntry extends ExpenseEntry {
  readonly status: ReimbursementStatus;
}

interface ExpenseReport {
  readonly entries: ReadonlyArray<TaggedExpenseEntry>;
  readonly sortedByAmount: ReadonlyArray<TaggedExpenseEntry>;
  readonly categorySummaries: ReadonlyArray<CategorySummary>;
  readonly grandTotalUSD: number;
  readonly rejected: ReadonlyArray<{ raw: RawExpenseEntry; error: ValidationError }>;
}

// --- Functions to implement ---
function validateExpenseEntry(raw: RawExpenseEntry): Result<ExpenseEntry, ValidationError> { ... }
function deriveStatus(entry: ExpenseEntry): ReimbursementStatus { ... }
function buildExpenseReport(raws: ReadonlyArray<RawExpenseEntry>): ExpenseReport { ... }
Hints (click to reveal)

Hints

  • When narrowing `unknown` fields, use `typeof` checks and `Array.prototype.includes` with a `const` tuple of valid categories to satisfy the type checker without casting.
  • For `buildExpenseReport`'s aggregation, a `Map<ExpenseCategory, CategorySummary>` makes grouping and updating per-category totals straightforward in a single pass.
  • The `Record<ReimbursementStatus['kind'], number>` in `CategorySummary.byStatus` needs all three keys (`auto_approved`, `needs_review`, `flagged`) initialised to `0` before counting.

Or clone locally

git clone -b challenge/2026-03-21 https://github.com/niltonheck/typedrop.git