TypeDrop
2026-07-02 Challenge
2026-07-02
Easy
Typed Expense Report Aggregator
You're building the expense-report processing module for a finance dashboard. Raw expense entries arrive as `unknown` from a form submission API; your module must validate them into strongly-typed `Expense` records, group them by category, compute per-category and overall totals, and return a fully-typed `ExpenseReport` — with zero `any`.
Goals
- Implement `isISODateString` and `isExpenseCategory` as proper type guards — the latter must derive its valid values from a single `const` tuple.
- Implement `parseExpense` to validate every field of a raw `unknown` entry, collecting ALL field-level errors before returning the discriminated-union result.
- Implement `buildExpenseReport` to parse all raw entries, group valid expenses by category using a `Map`, and return a fully-typed `ExpenseReport` with `byCategory` sorted A-Z.
- Achieve all of the above with zero `any`, zero `as` assertions, and zero TypeScript errors under `strict: true`.
challenge.ts
// Key types at a glance:
type ExpenseCategory = "travel" | "meals" | "accommodation" | "equipment" | "other";
interface Expense {
id: string; submittedBy: string; category: ExpenseCategory;
description: string; amountCents: number; date: string;
}
type ExpenseParseResult =
| { ok: true; expense: Expense }
| { ok: false; rawIndex: number; errors: ExpenseFieldError[] };
interface ExpenseReport {
validCount: number; invalidCount: number; totalCents: number;
byCategory: CategorySummary[];
parseErrors: Array<{ rawIndex: number; errors: ExpenseFieldError[] }>;
}
// Your two entry-point functions:
function parseExpense(raw: unknown, rawIndex: number): ExpenseParseResult { ... }
function buildExpenseReport(rawEntries: unknown[]): ExpenseReport { ... }
Hints (click to reveal)
Hints
- For `isExpenseCategory`, declare `const EXPENSE_CATEGORIES = ['travel', 'meals', ...] as const` and use `(EXPENSE_CATEGORIES as readonly string[]).includes(value as string)` — but you'll still need to narrow `value` to `string` first before calling `.includes`.
- In `parseExpense`, access `raw` fields safely by first checking `typeof raw === 'object' && raw !== null`, then using `'id' in raw` guards before reading each property — this keeps TypeScript happy without any `as` casts.
- Use `Map<ExpenseCategory, CategorySummary>` in `buildExpenseReport` to accumulate per-category counts and totals in a single pass, then spread `.values()` into an array and sort it.
Useful resources
Or clone locally
git clone -b challenge/2026-07-02 https://github.com/niltonheck/typedrop.git