Typorithm · hard

2. Generic inference and constraint design

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

2. Generic inference and constraint design

0%

Concept and contract

Preserve caller information while giving implementations only the capabilities they need.

Inference collects candidate types from arguments and context, then checks the generic implementation under its declared constraints. A constraint describes minimum capabilities; it does not replace the concrete inferred type or authorize a result that contains only those minimum members.

Prefer constraints on the actual operation you need. A function reading length can accept a readonly sequence or another length-bearing value, while a function indexing elements needs a more specific contract. Overconstraining makes reuse harder without adding a meaningful guarantee.

Mechanism and worked example

The example returns the original object after checking its id.

The example returns the original object after checking its id. T preserves additional caller fields, and the constraint makes reading id legal. A caller receives the same identity and the same extra property types, rather than a reconstructed minimal id object.

Inference also responds to contextual types and mutable versus readonly containers. Use readonly input arrays when the implementation does not mutate them. Keep an explicit return annotation where it documents an important relationship, especially in exported helpers and declaration output.

Complete worked example

typescript

function requireId<T extends { id: string }>(value: T): T {
  if (!value.id) throw new Error("Missing id");
  return value;
}
const record = { id: "a", salary: 10 };
const same = requireId(record);
Reasoning
The original object satisfies the constraint and retains its caller-specific properties.

Counterexamples and common mistakes

A fabricated { id } cannot satisfy every T extending an id contract.

A fabricated { id } cannot satisfy every T extending an id contract. A particular caller might require salary or an exact literal id. An assertion to T conceals this mismatch and makes the helper promise more information than it constructs.

Avoid adding generic parameters that appear only in constraints or a single output with no trustworthy input relationship. A caller-selected return generic on raw JSON is especially misleading: the caller can ask for a type that no runtime decoder ever established.

Rejected counterexample: expected compiler diagnostic

typescript

function fabricate<T extends { id: string }>(): T {
  // @ts-expect-error T may require more than id.
  return { id: "a" };
}
Reasoning
The constraint is a minimum, so constructing only that minimum cannot justify arbitrary T.

Independent practice

Implement requireFirst<T> accepting a readonly array and returning an existing T or throwing for an empty array.

Implement requireFirst<T> accepting a readonly array and returning an existing T or throwing for an empty array. Preserve a rich record’s fields and identity. Do not create a fallback object, because the implementation cannot manufacture an arbitrary caller-specific T.

Test empty input, a numeric input and a readonly record array. Explain why returning undefined is another reasonable API but would require a different return contract and caller behavior. Keep the exercise’s throwing policy explicit.

Step 1: Read index zero once. Step 2: Reject undefined and return the existing value.

Practice starter: solve before reading the solution

typescript

function requireFirst<T>(items: readonly T[]): T { throw new Error("TODO: reject undefined"); }
Reasoning
Choose explicit behavior for undefined elements as well as empty arrays.
  • Hint 1: The contract intentionally excludes an undefined first element.
  • Hint 2: Null is a defined value in this exercise.

Solution, tests and trade-offs

The solution reads one indexed value and checks for undefined.

The solution reads one indexed value and checks for undefined. This policy rejects an actual undefined element too, so the stated contract is a first defined value at index zero, not every conceivable T including undefined. A length-based implementation would need careful treatment of indexed uncertainty.

Test that behavior explicitly, or restrict the input domain if undefined must never be an element. This illustrates a deeper generic lesson: a helper’s runtime interpretation of absence must remain accurate for all supported instantiations, not merely the easiest record example.

Practice solution and acceptance checks

typescript

function requireFirst<T>(items: readonly T[]): T {
  const value = items[0];
  if(value === undefined) throw new Error("First defined item required");
  return value;
}

// Acceptance checks
const r={id:"a",salary:1};if(requireFirst([r])!==r||requireFirst([2])!==2)throw new Error("first");for(const a of [[],[undefined]]){let bad=false;try{requireFirst(a)}catch{bad=true}if(!bad)throw new Error("missing");}
Reasoning
The check rejects an undefined first value and returns the original defined T; it never fabricates caller-specific data.

Interview reasoning and next step

In interviews, explain why T extends Shape does not let you return Shape as T.

In interviews, explain why T extends Shape does not let you return Shape as T. Identify inference inputs and describe what information would be lost by replacing T with its constraint. Mention empty-collection behavior before presenting code.

The next lessons move these relationships into type transformations. Retain the same discipline: every advanced expression should encode an actual consumer relationship rather than merely produce an impressive-looking type.

Optional stretch — after the core task works: Compare a T | undefined result API with the throwing version.

Preserve caller information while giving implementations only the capabilities they need.