TypeDrop
2026-03-12 Challenge
2026-03-12
Easy
Typed Expense Report Summariser
You're building the finance module for a small business app. Raw expense entries arrive as unknown JSON from a mobile upload, and you must validate them, categorise them, and produce a per-category summary — all with zero `any` and fully typed results.
Goals
- Define a generic discriminated-union `Result<T, E>` type and use it as the return type of `validateExpense`.
- Implement `isExpenseCategory` as a proper TypeScript type guard that narrows `unknown` to `ExpenseCategory`.
- Write `validateExpense` so it collects ALL field-level `ValidationError`s in a single pass before returning `ok: false`.
- Wire everything together in `processRawExpenses`, separating valid expenses from invalid ones and building a sorted `ExpenseReport`.
challenge.ts
// Core domain types
type ExpenseCategory = "travel" | "meals" | "software" | "hardware" | "other";
interface Expense {
id: string;
description: string;
amountCents: number; // integer > 0
category: ExpenseCategory;
date: string; // "YYYY-MM-DD"
submittedBy: string;
}
// Result discriminated union — your job to define it!
type Result<T, E> = /* TODO */;
// Type guard — must narrow to ExpenseCategory
function isExpenseCategory(value: unknown): value is ExpenseCategory { /* TODO */ }
// Validates raw unknown → typed Expense, collecting ALL field errors
function validateExpense(raw: unknown): Result<Expense, ValidationError[]> { /* TODO */ }
// Groups a validated list into a Map keyed by category
function groupByCategory(expenses: Expense[]): Map<ExpenseCategory, Expense[]> { /* TODO */ }
// Orchestrates validation + grouping → final typed report
function processRawExpenses(rawEntries: unknown[]): {
report: ExpenseReport;
invalid: Array<{ raw: unknown; errors: ValidationError[] }>;
} { /* TODO */ }
Hints (click to reveal)
Hints
- A discriminated union on a boolean literal (`ok: true` vs `ok: false`) is the cleanest way to model `Result<T, E>` — TypeScript will narrow the type automatically in `if (result.ok)` branches.
- In `validateExpense`, check `typeof raw === 'object' && raw !== null` first, then use `'id' in raw` + `typeof (raw as Record<string, unknown>).id` to access each field safely without `any`.
- `Array.prototype.reduce` over `Map.entries()` is a clean single-pass way to compute `totalCents` and `count` for each `ExpenseSummary` inside `buildReport`.
Useful resources
Or clone locally
git clone -b challenge/2026-03-12 https://github.com/niltonheck/typedrop.git