TypeDrop

2026-06-28 Challenge

2026-06-28 Easy

Typed CSV Contact Importer with Validation & Deduplication

You're building the contact-import pipeline for a CRM tool. Raw CSV rows arrive as `unknown` from a file parser; your module must validate them into strongly-typed `Contact` records, collect structured field-level errors, and deduplicate entries by email — returning a fully-typed import report with zero `any`.

Goals

  • Define a branded `Email` type and use it throughout validation and deduplication without any type assertions.
  • Implement `isPlainObject` as a proper type guard that narrows `unknown` to `Record<string, unknown>`.
  • Implement `validateRow` to collect ALL field-level errors before returning, and return a correctly-shaped `RowResult` discriminated union.
  • Implement `importContacts` to produce a fully-typed `ImportReport` with deduplicated contacts and a unique `duplicates` list.
challenge.ts
// Key types and main function signature

type Email = string & { readonly _brand: "Email" };
type ContactCategory = "lead" | "customer" | "partner";

interface Contact {
  id: string;
  name: string;
  email: Email;
  category: ContactCategory;
  createdAt: Date;
}

interface FieldError { field: string; message: string; }

type RowResult =
  | { status: "ok";    contact: Contact }
  | { status: "error"; rowIndex: number; errors: [FieldError, ...FieldError[]] };

interface ImportReport {
  imported:   Contact[];
  failed:     RowResult[];
  duplicates: Email[];
}

function isPlainObject(value: unknown): value is Record<string, unknown> { … }
function validateRow(raw: unknown, rowIndex: number): RowResult { … }
function importContacts(rows: unknown[]): ImportReport { … }
Hints (click to reveal)

Hints

  • A branded type is just an intersection: `type Email = string & { readonly _brand: 'Email' }`. You can safely widen back to it inside a validated branch with a `return value as Email`... but can you avoid the cast entirely by structuring the return?
  • Collect errors into a `FieldError[]` array, then check `errors.length > 0` to decide which `RowResult` variant to return — TypeScript will accept `errors as [FieldError, ...FieldError[]]` only if you verify length first, or you can use a tuple push pattern.
  • Use a `Set<Email>` to track seen emails and a second `Set<Email>` to track already-recorded duplicates, so each dropped email appears at most once in `report.duplicates`.

Or clone locally

git clone -b challenge/2026-06-28 https://github.com/niltonheck/typedrop.git