Typorithm · medium

9. Generics, constraints and preserved relationships

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

9. Generics, constraints and preserved relationships

0%

Concept and contract

Use type parameters to connect values instead of replacing uncertainty with any.

A generic function describes a relationship that remains valid for multiple concrete types. In identity<T>(value: T): T, the same T connects input and output. Returning any would accept many callers but lose the relationship that makes their results useful.

A constraint limits the operations the implementation may perform. T extends { id: string } promises an id property, while retaining additional information about the actual caller’s type. A constraint does not grant permission to manufacture an arbitrary T from only the constrained fields.

Mechanism and worked example

The example finds an item by id and returns T | undefined.

The example finds an item by id and returns T | undefined. Inference learns T from the collection, so a caller using salary-bearing roles retains salary in the successful result. Absence stays explicit because a matching id is not guaranteed.

The implementation reads only id, which the constraint establishes, and returns an existing item unchanged. It does not assume every T has salary. Generic code must work for every permitted instantiation, not only the first concrete type used in a demonstration.

Complete worked example

typescript

function byId<T extends { id: string }>(items: readonly T[], id: string): T | undefined {
  return items.find(item => item.id === id);
}
const found = byId([{ id: "r1", salary: 100 }], "r1");
Reasoning
The implementation uses only the constraint while the returned existing element preserves additional caller fields.

Counterexamples and common mistakes

The rejected call supplies objects without the constrained id capability.

The rejected call supplies objects without the constrained id capability. Adding an assertion would defeat the contract. A subtler mistake is returning { id } as T: callers may have required fields that this fabricated value does not possess.

Avoid introducing a type parameter that appears only once and establishes no useful relationship. unknown often describes an unconstrained input more honestly. Start with a concrete contract, identify the information callers need preserved, and add only the parameter that connects those positions.

Rejected counterexample: expected compiler diagnostic

typescript

function byId<T extends { id: string }>(items: readonly T[], id: string): T | undefined { return items.find(x => x.id === id); }
// @ts-expect-error The element lacks id.
byId([{ salary: 100 }], "r1");
Reasoning
A constraint requires the capability used by the implementation.

Independent practice

Implement firstMatching for a readonly collection and a predicate receiving its element type.

Implement firstMatching for a readonly collection and a predicate receiving its element type. Return the first matching element or undefined without copying or mutating it. Use both a numeric collection and a richer role record to demonstrate preserved inference.

Reject a predicate that expects the wrong element type. Test no match and an empty input separately. Do not default to the first element when no predicate succeeds, because that would silently change the function’s meaning while making the return type look simpler.

Step 1: Connect items and predicate through T. Step 2: Return the matching existing item or undefined.

Practice starter: solve before reading the solution

typescript

function firstMatching<T>(items: readonly T[], test: (value: T) => boolean): T | undefined { throw new Error("TODO"); }
Reasoning
Connect collection, predicate and optional output through the same type parameter.
  • Hint 1: find already expresses the no-match policy.
  • Hint 2: Do not manufacture a fallback T.

Solution, tests and trade-offs

The solution delegates to find and connects the input element type, callback parameter and optional output through T.

The solution delegates to find and connects the input element type, callback parameter and optional output through T. The runtime algorithm is unchanged across instantiations. Type arguments are erased, so this reuse adds a static relationship rather than runtime specialization.

Check object identity for a returned record, not only matching fields. Then assign a successful numeric result only after handling undefined. These checks exercise both preservation and honest absence; neither requires inspecting a caller-specific property inside the generic implementation.

Practice solution and acceptance checks

typescript

function firstMatching<T>(items: readonly T[], test: (value: T) => boolean): T | undefined {
  return items.find(test);
}

// Acceptance checks
const item={id:"a",salary:3};if(firstMatching([item],x=>x.salary>0)!==item||firstMatching([1,2],x=>x>5)!==undefined||firstMatching([],()=>true)!==undefined)throw new Error("find");
Reasoning
find preserves the original element reference and represents missing matches explicitly.

Interview reasoning and next step

For an interview, explain where inference obtains each type parameter and what the constraint permits.

For an interview, explain where inference obtains each type parameter and what the constraint permits. Show why a fabricated object satisfying the constraint need not satisfy T. Distinguish a reusable generic from a broad function returning unknown.

Next, keyof and indexed access let generics preserve the relationship between a chosen property name and that property’s value type. This is the foundation of useful typed lookup and update helpers.

Optional stretch — after the core task works: Add a find-all variant and test identity preservation.

Use type parameters to connect values instead of replacing uncertainty with any.