TypeDrop

2026-05-04 Challenge

2026-05-04 Easy

Typed Expense Report Builder

You're building the expense-reporting layer for a travel management app. Raw expense entries arrive as `unknown` from an employee submission form; your engine must validate them, group them by category, and return a strongly-typed report summary — with zero `any`.

Goals

  • Implement `isExpenseCategory` as a type predicate that narrows `unknown` to `ExpenseCategory`.
  • Implement `validateExpenseEntry` to validate all required fields of a raw unknown payload and return a discriminated-union `ValidationResult`.
  • Implement `buildExpenseReport` to validate every raw entry, group valid ones into a `CategorySummary` (all five keys always present), and compute the grand total.
  • Ensure every `ExpenseCategory` key exists in `categorySummary` even when no entries belong to that category.
challenge.ts
export type ExpenseCategory =
  | "travel" | "meals" | "accommodation" | "equipment" | "other";

export interface ExpenseEntry {
  id: string;
  employeeId: string;
  category: ExpenseCategory;
  amountCents: number;   // positive integer, in cents
  date: string;          // "YYYY-MM-DD"
  description: string;
}

export type CategorySummary = Record<
  ExpenseCategory,
  { totalCents: number; count: number }
>;

export interface ExpenseReport {
  entries: ExpenseEntry[];
  categorySummary: CategorySummary;
  grandTotalCents: number;
  failures: ValidationFailure[];
}

// Your main entry point:
export function buildExpenseReport(rawEntries: unknown[]): ExpenseReport {
  // TODO
  throw new Error("Not implemented");
}
Hints (click to reveal)

Hints

  • A type predicate has the form `(value: unknown): value is SomeType` — use it on `isExpenseCategory` so the compiler trusts the narrowing downstream.
  • Seed your `categorySummary` object by iterating over a const array of all five `ExpenseCategory` values before processing any entries — this guarantees every key is present.
  • When checking `amountCents`, combine `typeof x === 'number'`, `x > 0`, and `Math.trunc(x) === x` to satisfy the positive-integer requirement without casting.

Or clone locally

git clone -b challenge/2026-05-04 https://github.com/niltonheck/typedrop.git