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`.
Useful resources
Or clone locally
git clone -b challenge/2026-06-28 https://github.com/niltonheck/typedrop.git