TypeDrop

2026-03-17 Challenge

2026-03-17 Easy

Typed Book Club Reading List Builder

You're building the reading list feature for a book club app. Members submit raw book entries from a form, and you must validate them, tag each book with a derived reading status, and produce a sorted, fully typed reading list — with zero `any`.

Goals

  • Implement `ok` and `err` generic helpers that construct a discriminated-union `Result<T, E>`.
  • Write `isGenre` as a type-predicate function that narrows `unknown` to the `Genre` union.
  • Implement `parseBook` to validate every field of an `unknown` input against the `Book` interface, deriving `status` via `deriveStatus` and returning the first `BookValidationError` on failure.
  • Implement `buildReadingList` to parse all raw entries, sort valid books case-insensitively by title, and produce a `summary` `Record` with a count for every `ReadingStatus` key — even zeros.
challenge.ts

export type Genre = "fiction" | "non-fiction" | "biography" | "science" | "history";
export type ReadingStatus = "not-started" | "in-progress" | "completed";

export interface Book {
  id: string;
  title: string;
  author: string;
  genre: Genre;
  totalPages: number;
  pagesRead: number;
  status: ReadingStatus; // derived — NOT accepted from raw input
}

export type Result<T, E> =
  | { ok: true;  value: T }
  | { ok: false; error: E };

export interface ReadingList {
  books: Book[];                               // sorted A-Z by title
  summary: Record<ReadingStatus, number>;      // count per status (all keys present)
  rejected: Array<{ input: unknown; error: BookValidationError }>;
}

// Main entry point
export function buildReadingList(rawEntries: unknown[]): ReadingList { ... }
Hints (click to reveal)

Hints

  • A type-predicate (`value is Genre`) lets TypeScript narrow the type in the caller's scope — use an array of the valid strings and `.includes()` after a `typeof` check.
  • When building the `summary` Record, initialise all three `ReadingStatus` keys to `0` before iterating so no key is ever missing — `Record<ReadingStatus, number>` requires all keys.
  • In `parseBook`, check `typeof input === 'object' && input !== null` first, then use `'id' in input` before indexing — this keeps you `any`-free under `strict: true`.

Or clone locally

git clone -b challenge/2026-03-17 https://github.com/niltonheck/typedrop.git