TypeDrop

2026-03-02 Challenge

2026-03-02 Easy

Typed Contact Book with Safe Parsing & Lookup

You're building a lightweight contact management module for a small business app. Raw contact data arrives as unknown JSON from an import file, and you must validate it into strongly-typed records, build an efficient lookup index, and expose typed query helpers — all without reaching for `any`.

Goals

  • Define a generic `Result<T, E>` discriminated union and a `ParseError` discriminated union with at least three variants.
  • Implement `parseContact` to safely narrow `unknown` input into a typed `Contact`, returning descriptive `ParseError` values for every invalid case.
  • Implement `buildIndex` using the `Record` utility type and `findById` returning `Contact | undefined` without any type assertions.
  • Implement `filterByTags` and `searchByName` using typed iteration over the `ContactIndex`.
challenge.ts
export type PhoneType = "mobile" | "home" | "work";

export interface PhoneNumber {
  type: PhoneType;
  number: string;
}

export interface Contact {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
  phones: PhoneNumber[]; // at least one entry
  tags: string[];
}

export type Result<T, E> = never; // TODO: define discriminated union

export type ParseError = never;   // TODO: define discriminated union

export type ContactIndex = never; // TODO: use Record utility type

export function parseContact(raw: unknown): Result<Contact, ParseError> { ... }
export function buildIndex(contacts: Contact[]): ContactIndex { ... }
export function findById(index: ContactIndex, id: string): Contact | undefined { ... }
export function filterByTags(index: ContactIndex, requiredTags: string[]): Contact[] { ... }
export function searchByName(index: ContactIndex, prefix: string): Contact[] { ... }
Hints (click to reveal)

Hints

  • A discriminated union needs a shared literal-typed field (e.g. `ok` or `kind`) so TypeScript can narrow between variants in `if`/`switch` blocks.
  • To validate `phones` entries, check `Array.isArray` first, then iterate and verify each entry's `type` is one of the three `PhoneType` literals — a `const VALID_PHONE_TYPES` tuple with `includes` (after a cast-free helper) works well here.
  • `Object.values(index)` gives you a `Contact[]` you can then `filter` and `sort` without any extra type work.

Or clone locally

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