TypeDrop
2026-04-06 Challenge
2026-04-06
Easy
Typed Contact Book Grouper
You're building the "All Contacts" view for a mobile address book app. Raw contact entries arrive from storage as unknown blobs; your engine must validate them, group them by the first letter of their last name, and return a fully typed alphabetical index — with zero `any`.
Goals
- Define `AlphaKey` as a union of all 26 uppercase letter strings plus "#", and `ContactIndex` as a `Record<AlphaKey, Contact[]>`.
- Implement `validateContact` to narrow an `unknown` blob into a typed `Contact`, returning a discriminated `Result<Contact, ValidationError>` with the first error encountered.
- Implement `getAlphaKey` to map a contact's last name to the correct `AlphaKey` bucket (A–Z or "#").
- Implement `buildContactIndex` so every one of the 27 keys is always present, invalid entries are silently skipped, and each bucket is sorted by `lastName` then `firstName`.
challenge.ts
/** A validated, fully-typed contact record. */
interface Contact {
id: string;
firstName: string;
lastName: string;
email: string;
phone?: string;
birthday?: string;
tags: string[];
}
type ValidationError =
| { kind: "missing_field"; field: string }
| { kind: "wrong_type"; field: string; expected: string }
| { kind: "invalid_email"; value: string };
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
// TODO: define AlphaKey → "A" | "B" | … | "Z" | "#"
type AlphaKey = TODO;
// TODO: define ContactIndex → Record<AlphaKey, Contact[]>
type ContactIndex = TODO;
// Validate a raw unknown blob into a typed Contact
function validateContact(raw: unknown): Result<Contact, ValidationError> { … }
// Map a Contact to its alphabetical bucket key
function getAlphaKey(contact: Contact): AlphaKey { … }
// Build a complete A–Z + "#" index from an array of raw blobs
function buildContactIndex(raws: unknown[]): ContactIndex { … }
Hints (click to reveal)
Hints
- For `AlphaKey`, you can write it as an explicit union `"A" | "B" | ... | "Z" | "#"` — or use a mapped type over a tuple of letters with `infer` if you want a stretch challenge.
- In `validateContact`, narrow `raw` step-by-step: first check `typeof raw === 'object' && raw !== null`, then use `'id' in raw` before accessing each field — TypeScript will track the narrowed type for you.
- To guarantee all 27 keys exist in `buildContactIndex`, initialise the accumulator with `Object.fromEntries(expectedKeys.map(k => [k, []]))` and cast it with `satisfies ContactIndex` rather than `as`.
Useful resources
Or clone locally
git clone -b challenge/2026-04-06 https://github.com/niltonheck/typedrop.git