TypeDrop

2026-09-19 Challenge

2026-09-19 Easy

Typed Expense Report Summarizer

You're building the core logic for a corporate expense management tool. Finance teams upload raw expense entries and need a strongly-typed summary grouped by category, with per-category totals and an overall grand total — all shapes enforced by the compiler.

Goals

  • Define `PartialCategoryMap<V>` as a mapped type over `ExpenseCategory` with optional values, and `NumericExpenseFields` using key remapping with a conditional type.
  • Implement `extractCategories` to return unique `ExpenseCategory` values in first-seen order without widening the type.
  • Implement `groupByCategory` and `summarizeCategory` to correctly group expenses and compute per-category totals, counts, and the largest expense.
  • Implement `buildExpenseReport` to compose the above helpers into a full `ExpenseReport`, including grand total, entry count, and ISO date range.
challenge.ts
export type ExpenseCategory =
  | "travel" | "meals" | "software"
  | "hardware" | "office" | "other";

export interface Expense {
  id: string;
  submittedBy: string;
  category: ExpenseCategory;
  amountUSD: number;
  description: string;
  date: string; // ISO 8601
}

export interface CategorySummary {
  category: ExpenseCategory;
  totalUSD: number;
  count: number;
  largestExpense: Expense;
}

export interface ExpenseReport {
  byCategory: Record<ExpenseCategory, CategorySummary>;
  grandTotalUSD: number;
  totalCount: number;
  earliestDate: string | null;
  latestDate: string | null;
}

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

Hints

  • For `NumericExpenseFields`, use a mapped type with `as` key remapping: `[K in keyof Expense as Expense[K] extends number ? K : never]`.
  • For `extractCategories`, a `Set<ExpenseCategory>` guarantees uniqueness while preserving insertion order — spread it into an array to return.
  • For `buildExpenseReport`, build a `PartialCategoryMap<CategorySummary>` first, then cast it to `Record<ExpenseCategory, CategorySummary>` only at the final assignment — this is the one safe structural cast the design allows.

Or clone locally

git clone -b challenge/2026-09-19 https://github.com/niltonheck/typedrop.git