Typorithm · hard

16. Capstone: a typed role explorer

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: a typed role explorer

0%

Concept and contract

Connect a validated boundary, React state and a small independent filter task.

The final project combines a role decoder, controlled query state and a rendered list. Keep the first version small enough to understand end to end. A role has id, title and a finite country code; the UI should never assert unknown API data directly into that contract.

Start from a trusted fixture and working filter, then add an injected fetch boundary and explicit loading or failure state. This sequence keeps each step approachable. Network requests, server permissions and persistence are extensions rather than hidden prerequisites to completing the basic interface.

Mechanism and worked example

The worked component renders trusted Role props, keeps query in state and derives visible roles directly during rendering.

The worked component renders trusted Role props, keeps query in state and derives visible roles directly during rendering. It does not need an effect to copy filtered results into another state variable. This avoids redundant synchronization and makes the filter’s inputs obvious.

The decoder belongs before the component receives roles. For real asynchronous loading, reuse the request-state lesson’s stale-response policy and typed-fetch lesson’s status handling. An error message should distinguish unavailable transport from malformed data without exposing raw private payloads.

Complete worked example

typescript

import {useState} from "react";
type Role={id:string;title:string;country:"US"|"UA"};
export function RoleExplorer({roles}:{roles:readonly Role[]}){
  const [query,setQuery]=useState("");
  const visible=roles.filter(role=>role.title.toLowerCase().includes(query.trim().toLowerCase()));
  return <section><label>Search roles<input value={query} onChange={event=>setQuery(event.currentTarget.value)}/></label>
    {visible.length?<ul>{visible.map(role=><li key={role.id}>{role.title}</li>)}</ul>:<p>No matching roles</p>}</section>;
}
Reasoning
The component receives trusted data and derives filtered results rather than synchronizing duplicate state through an effect.

Counterexamples and common mistakes

The rejected props omit country from a role.

The rejected props omit country from a role. That field is required by the displayed domain even if the first UI only shows title. Weakening it to optional would move missing-country uncertainty into every filter and label rather than repairing the boundary.

Do not claim the whole app is verified because its JSX compiles. Check typing in the search input, empty results, stable list keys and selection behavior. If saved state is added, test storage and account scoping separately instead of assuming useState persists it.

Rejected counterexample: expected compiler diagnostic

typescript

type Role={id:string;title:string;country:"US"|"UA"};
// @ts-expect-error Trusted roles require a country.
const invalid:Role={id:"a",title:"QA"};
Reasoning
The missing field is a boundary problem, not a reason to weaken every downstream role contract.

Independent practice

Implement visibleRoles as a pure function taking readonly roles, a query and either one country or all.

Implement visibleRoles as a pure function taking readonly roles, a query and either one country or all. Search title case-insensitively after trimming the query; combine query and country with AND. Begin with country-only filtering, then add query matching.

Test empty query, mixed case, no results and a combined filter that matches exactly one role. Optional extension: connect this helper to the supplied component and add an accessible country select. No recursive types or generic component framework is required.

Step 1: Filter one country first. Step 2: Add normalized title matching with AND semantics.

Practice starter: solve before reading the solution

typescript

type Role={id:string;title:string;country:"US"|"UA"};
function visibleRoles(roles:readonly Role[],query:string,country:Role["country"]|"all"):Role[]{throw new Error("TODO");}
Reasoning
Build the country predicate first, then combine it with normalized title search.
  • Hint 1: Empty query should preserve all country-matching roles.
  • Hint 2: Derive visible data rather than copying it into effect-managed state.

Solution, tests and trade-offs

The solution normalizes query once and applies both predicates without mutating the source.

The solution normalizes query once and applies both predicates without mutating the source. The concrete contract is clearer than a generic query language for this small feature. Its output stays Role[] because filtering preserves the trusted record shape.

Use an independent expected id list for each test, then mount the component and exercise the real controls. A complete review should follow raw fixture through decoding, filtering and rendering. Each layer contributes different evidence and should fail clearly when its own assumption breaks.

Practice solution and acceptance checks

typescript

type Role={id:string;title:string;country:"US"|"UA"};
function visibleRoles(roles:readonly Role[],query:string,country:Role["country"]|"all"):Role[]{
  const normalized=query.trim().toLowerCase();
  return roles.filter(role=>(country === "all" || role.country === country)&&role.title.toLowerCase().includes(normalized));
}

// Acceptance checks
const roles:Role[]=[{id:"a",title:"QA Engineer",country:"UA"},{id:"b",title:"QA Lead",country:"US"},{id:"c",title:"Designer",country:"UA"}];if(visibleRoles(roles," qa ","UA").map(x=>x.id).join(",")!=="a"||visibleRoles(roles,"","all").length!==3||visibleRoles(roles,"none","all").length!==0||roles.length!==3)throw new Error("combined filter");
Reasoning
The pure filter preserves trusted records and uses explicit AND semantics for combined selection.

Interview reasoning and next step

For an interview presentation, explain the boundary, ownership, derived state and filter semantics before discussing optimization.

For an interview presentation, explain the boundary, ownership, derived state and filter semantics before discussing optimization. Show one malformed API fixture, one rejected prop and one user interaction. Describe what remains outside the project’s scope honestly.

You now have a connected track from basic inference to advanced type relationships and a practical React boundary. Revisit the independent tasks without the solutions, and use the optional extensions only after the smaller contract is comfortable and correctly tested.

Optional stretch — after the core task works: Wire an accessible country select into the supplied component.

Connect a validated boundary, React state and a small independent filter task.