TypeDrop

2026-09-10 Challenge

2026-09-10 Easy

Typed Expense Splitter with Settlement Calculation

You're building the core logic for a group-trip expense-splitting app. Participants log expenses paid on behalf of the group, and the app must compute each person's net balance and produce the minimal list of cash transfers needed to settle all debts — with the compiler enforcing every shape along the way.

Goals

  • Define `SplitError` as a discriminated union with `kind` variants: `"NO_PARTICIPANTS"`, `"UNKNOWN_PAYER"`, and `"EMPTY_SPLIT"`, each carrying a `message` string.
  • Implement `asCents` to brand a plain number as `Cents`, throwing `RangeError` for non-finite or non-integer inputs.
  • Implement `computeSplit` to validate inputs, compute per-participant net balances in whole cents (distributing remainder to the first participant), and greedily produce a minimal list of settlement transfers.
  • Implement `formatSummary` using type narrowing on `result.ok` to produce a human-readable string — either an error message or dollar-formatted transfer lines.
challenge.ts
// Key types & main function signature

type Cents = number & { readonly __brand: "Cents" };

interface Expense {
  readonly id: string;
  readonly description: string;
  readonly paidBy: string;
  readonly amount: Cents;
  readonly splitAmong: readonly string[];
}

interface Balance {
  readonly participant: string;
  readonly netCents: Cents;   // positive = owed money, negative = owes money
}

interface Transfer {
  readonly from: string;
  readonly to: string;
  readonly amountCents: Cents;
}

type SplitResult =
  | { readonly ok: true;  readonly balances: readonly Balance[]; readonly transfers: readonly Transfer[] }
  | { readonly ok: false; readonly error: SplitError };

// Your job: define SplitError, then implement these ↓
declare function asCents(n: number): Cents;
declare function computeSplit(participants: readonly string[], expenses: readonly Expense[]): SplitResult;
declare function formatSummary(result: SplitResult): string;
Hints (click to reveal)

Hints

  • A branded type like `Cents = number & { readonly __brand: "Cents" }` cannot be created with a cast — use a dedicated factory function (`asCents`) that validates and returns the branded value.
  • For the greedy settlement loop, sort creditors and debtors by absolute value descending, then repeatedly pair the largest creditor with the largest debtor, emit a Transfer for `Math.min(credit, debt)`, and update both sides until all are zero.
  • In `formatSummary`, narrow with `if (result.ok)` — the compiler will then know the `true` branch has `balances` and `transfers`, and the `false` branch has `error`, with no type assertions needed.

Or clone locally

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