TypeDrop

A new TypeScript challenge every day. Sharpen your types.

TypeDrop delivers a fresh TypeScript challenge every day, generated by AI. Pick a challenge, open it in StackBlitz (preferred) or CodeSandbox (or clone it locally), and make the tests pass. No accounts, no setup — just you and the type system.

Learn more on GitHub →

2026-06-19 Medium

Typed CSV Report Pipeline with Aggregation & Validation

You're building the data-export layer for an analytics dashboard. Raw CSV rows arrive as `unknown[]` from a file-upload parser; your pipeline must validate each row into a strongly-typed record, aggregate metrics in a single pass, and return a typed report summary — with zero `any`.

Goals

  • Implement `validateRow` to fully narrow `unknown` → `SalesRecord` using field-by-field type guards, returning the first failing field in a typed `ValidationFailure`.
  • Implement `aggregateRecords` to compute `grandTotalRevenue` and a per-region `Record<Region, RegionSummary>` in a single loop pass, with all monetary values rounded to 2 decimal places.
  • Implement `runPipeline` to orchestrate validation and aggregation, collecting errors and returning a fully-typed `PipelineResult` discriminated union — never throwing.
  • Ensure all four `Region` values always appear in `byRegion` (even if a region has zero records), using `Record<Region, RegionSummary>`.
challenge.ts

// Key types & main function signatures at a glance

export type Region = "north" | "south" | "east" | "west";

export interface SalesRecord {
  id: string;
  region: Region;
  product: string;
  quantity: number;   // positive integer
  unitPrice: number;  // positive number
  saleDate: string;   // "YYYY-MM-DD"
}

export type ValidationResult<T> =
  | { readonly status: "ok";    readonly value: T }
  | { readonly status: "error"; readonly rowIndex: number;
      readonly field: keyof SalesRecord | "unknown"; readonly message: string };

export type PipelineResult =
  | { status: "complete"; report: ReportSummary }
  | { status: "empty";    message: string }
  | { status: "fatal";    error: string };

export function validateRow(row: unknown, rowIndex: number): ValidationResult<SalesRecord>;

export function aggregateRecords(records: SalesRecord[]): Pick<ReportSummary, "grandTotalRevenue" | "byRegion">;

export function runPipeline(rawRows: unknown[]): PipelineResult;
Hints (click to reveal)

Hints

  • To narrow `unknown` to an object with known keys, first check `typeof row === 'object' && row !== null`, then use `'fieldName' in row` before accessing each property — this keeps TypeScript happy with no `as` or `any`.
  • For the `Region` guard, define a `const REGIONS = ['north','south','east','west'] as const` array and use `REGIONS.includes(value as Region)` — or write an explicit type-predicate function `isRegion(v: unknown): v is Region`.
  • Seed `byRegion` before your aggregation loop by mapping over all four region literals so every key is always present; `Math.round(x * 100) / 100` is a clean way to round to 2 decimal places.

Or clone locally

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