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.
Useful resources
Or clone locally
git clone -b challenge/2026-03-21 https://github.com/niltonheck/typedrop.git