Typorithm · medium

16. Capstone: validate and filter a role catalogue

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

16. Capstone: validate and filter a role catalogue

0%

Concept and contract

Build a small end-to-end boundary with honest results, immutable filtering and regression checks.

This capstone turns uncertain external records into a usable role catalogue. The trusted Role contract requires id, title and a supported country code. Inputs arrive as unknown because static annotations cannot establish what an external feed actually supplied.

Use a parser returning a tagged result rather than casting the incoming array. Keep validation, duplicate handling and filtering as separate responsibilities. The small fixture covers US and UA only as an exercise vocabulary; it does not claim those are the product’s only supported countries.

Mechanism and worked example

The worked parser checks one role and reports a failure instead of manufacturing defaults.

The worked parser checks one role and reports a failure instead of manufacturing defaults. It validates the country discriminant explicitly. A missing title should not become an empty title silently, because that would convert malformed input into apparently valid data.

For a collection, validate each element, preserve input order and choose a documented duplicate policy. Deduplicating by stable id is a business rule, not a property of TypeScript generics. A title match alone is insufficient evidence that two postings represent the same role.

Complete worked example

typescript

type Role = { id: string; title: string; country: "US" | "UA" };
function parseRole(value: unknown): Role | undefined {
  if (typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string"
    || !("title" in value) || typeof value.title !== "string" || !("country" in value)
    || (value.country !== "US" && value.country !== "UA")) return undefined;
  return { id: value.id, title: value.title, country: value.country };
}
Reasoning
Each returned Role is constructed only after the runtime checks justify all required fields.

Counterexamples and common mistakes

The rejected assignment shows that a raw string is not automatically a supported country.

The rejected assignment shows that a raw string is not automatically a supported country. Avoid assertions that treat every feed location as canonical. An international production catalogue needs a real location parser and source provenance beyond this deliberately small learning fixture.

Do not mutate the source array while filtering, and do not drop parse failures without reporting that records were withheld. If the caller needs partial success, model accepted items and issues separately instead of calling the whole operation fully successful.

Rejected counterexample: expected compiler diagnostic

typescript

type Country = "US" | "UA";
const raw: string = "US";
// @ts-expect-error External text has not been validated.
const country: Country = raw;
Reasoning
A broad external string is not proof of membership in a closed country vocabulary.

Independent practice

Implement importRoles for unknown input.

Implement importRoles for unknown input. Require an array, validate every record, deduplicate by id with the first valid occurrence winning, and return accepted records plus an issue count. Then implement countryFilter over readonly trusted roles.

Test a duplicate id, a malformed record, both supported countries and a nonarray input. Keep duplicate count separate from malformed count if your chosen result exposes both. The provided solution reports malformed records as issues and ignores later valid duplicates deliberately.

Step 1: Validate each record before recording its id. Step 2: Keep the first valid id and filter the trusted collection.

Practice starter: solve before reading the solution

typescript

type Role = { id: string; title: string; country: "US" | "UA" };
function importRoles(raw: unknown): { roles: Role[]; issues: number } { throw new Error("TODO"); }
Reasoning
Validate before deduplication, preserve ordering and make malformed-record reporting explicit.
  • Hint 1: An invalid earlier record must not suppress a valid later one.
  • Hint 2: Query filtering must not mutate imported order.

Solution, tests and trade-offs

The solution validates each record before adding its id to the seen set, so an invalid earlier record does not block a valid later one.

The solution validates each record before adding its id to the seen set, so an invalid earlier record does not block a valid later one. The filter returns a new collection without changing imported order. Explicit checks keep the static Role contract justified by runtime evidence.

Review empty input, duplicate ordering and missing fields, then compile a negative country call. Explain the limitations: no network fetching, full international normalization or cross-source semantic deduplication is implemented here. The capstone teaches the boundary pattern without disguising a toy parser as production ingestion.

Practice solution and acceptance checks

typescript

type Role = { id: string; title: string; country: "US" | "UA" };
function importRoles(raw: unknown): { roles: Role[]; issues: number } {
  if (!Array.isArray(raw)) return { roles: [], issues: 1 };
  const roles: Role[] = []; const seen = new Set<string>(); let issues = 0;
  for (const value of raw as unknown[]) {
    if (typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string"
      || !("title" in value) || typeof value.title !== "string" || !("country" in value)
      || (value.country !== "US" && value.country !== "UA")) { issues++; continue; }
    if (!seen.has(value.id)) { seen.add(value.id); roles.push({id:value.id,title:value.title,country:value.country}); }
  }
  return { roles, issues };
}
function countryFilter(roles: readonly Role[], country: Role["country"]): Role[] { return roles.filter(role => role.country === country); }

// Acceptance checks
const x=importRoles([{id:"a",title:"First",country:"UA"},{id:"a",title:"Duplicate",country:"US"},null,{id:"b",title:"B",country:"US"}]);if(x.roles.length!==2||x.issues!==1||countryFilter(x.roles,"UA")[0]?.title!=="First"||importRoles(null).issues!==1)throw new Error("import");
Reasoning
The solution keeps seen ids only for accepted records and filters without mutating the imported collection.

Interview reasoning and next step

Present the pipeline in an interview: raw value, validation result, deduplication policy, trusted collection and immutable query.

Present the pipeline in an interview: raw value, validation result, deduplication policy, trusted collection and immutable query. Identify which guarantees come from control flow, which from types and which from runtime assertions.

Move to the type-design course once you can rebuild this pipeline without copying the solution. That course generalizes the relationships while keeping the same rule: advanced static types must not invent runtime evidence that the program never checked.

Optional stretch — after the core task works: Return per-record issue categories instead of only a count.

Build a small end-to-end boundary with honest results, immutable filtering and regression checks.