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.

Or clone locally

git clone -b challenge/2026-10-23 https://github.com/niltonheck/typedrop.git