TypeDrop
2026-10-23 Challenge
2026-10-23
Easy
Typed Contact Book Aggregator
You're building the data layer for a personal contact book app. Contacts arrive from multiple sources (manual entry, email import, phone sync), and you need to group, search, and summarize them — with the compiler enforcing every field name, group key, and summary shape.
Goals
- Define `ContactSortField` using a mapped + conditional type that extracts only the `string | Date` keys of `Contact`, excluding nullable fields.
- Define `ContactSummary` with utility types (`Omit`/`Pick`) — never manually re-list property names — and add a `displayName` field via intersection.
- Implement `groupBySource` so that every `ContactSource` key is always present in the result, even when its contact list is empty.
- Implement `searchContacts` with a non-empty tuple parameter type so the compiler rejects an empty `fields` array at the call site.
challenge.ts
// Key types & main function signatures
export type ContactSource = "manual" | "email" | "phone";
export interface Contact {
id: string;
firstName: string;
lastName: string;
email: string;
phone: string | null; // nullable — should be excluded from sortable fields
source: ContactSource;
tags: string[];
createdAt: Date;
}
// Your job: fill in these type aliases correctly
export type ContactSortField = /* ??? — only string | Date fields of Contact */;
export type ContactSummary = /* ??? — Omit phone & tags, add displayName */;
export type GroupedContacts = /* ??? — Record keyed by ContactSource */;
export function toSummary(contact: Contact): ContactSummary;
export function sortContacts(contacts: Contact[], field: ContactSortField): Contact[];
export function groupBySource(contacts: Contact[]): GroupedContacts;
export function searchContacts(
contacts: Contact[],
query: string,
fields: [SearchableField, ...SearchableField[]] // non-empty tuple
): Contact[];
Hints (click to reveal)
Hints
- For `ContactSortField`, use a distributive mapped type: map each key to itself or `never`, then index with `[keyof Contact]` to collect the keepers.
- For `ContactSummary`, `Omit<Contact, 'tags' | 'phone'> & { displayName: string }` is the idiomatic two-step — no manual property listing needed.
- To guarantee all `ContactSource` keys exist in `groupBySource`, seed your accumulator with `{ manual: [], email: [], phone: [] }` before iterating.
Useful resources
Or clone locally
git clone -b challenge/2026-10-23 https://github.com/niltonheck/typedrop.git