TypeDrop

2026-09-05 Challenge

2026-09-05 Easy

Typed Contact Book with Grouped Search & Formatted Output

You're building the contact-management module for a small business CRM. Contacts can be reached by phone, email, or both, and the UI needs to search contacts by name fragment, group results by their first letter, and format each match into a display-ready summary string — all with the compiler enforcing every shape.

Goals

  • Define a `ContactChannel` discriminated union and a `NonEmptyArray<T>` generic tuple type.
  • Implement `buildContactBook` and `searchContacts` using a `ReadonlyMap` and a case-insensitive, multi-key sort.
  • Implement `groupByFirstLetter` that buckets contacts by the first letter of their last name, using '#' for non-alpha.
  • Implement an exhaustive `formatChannel` switch (proven by a `never` guard) and a multi-line `formatContact` formatter.
challenge.ts
// Core types — understand these before writing any logic

// A contact can be reached via phone or email (discriminated union)
type ContactChannel =
  | { kind: "phone"; number: string }
  | { kind: "email"; address: string };

// At least one channel is required
type NonEmptyArray<T> = [T, ...T[]];

interface Contact {
  id: string;
  firstName: string;
  lastName: string;
  channels: readonly ContactChannel[]; // NonEmptyArray enforced at call site
  tags: readonly string[];
}

type ContactBook = ReadonlyMap<string, Contact>;

// Main entry points you must implement:
function buildContactBook(contacts: NonEmptyArray<Contact>): ContactBook { ... }
function searchContacts(book: ContactBook, query: string): Contact[] { ... }
function groupByFirstLetter(contacts: Contact[]): Record<string, Contact[]> { ... }
function formatChannel(channel: ContactChannel): string { ... }  // exhaustive!
function formatContact(contact: Contact): string { ... }
Hints (click to reveal)

Hints

  • For `NonEmptyArray<T>`, tuple rest syntax `[T, ...T[]]` tells the compiler the array has at least one element — no runtime check needed.
  • In your exhaustive `switch` inside `formatChannel`, add a `default` branch that assigns `channel` to a `never` variable — the compiler will error if you miss a case.
  • A `ReadonlyMap` is constructed from a regular `new Map(...)` — you can widen the return type via the function's declared return type without any cast.

Or clone locally

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