Generic Constraints

The Course
•
5 min read

The previous lesson’s generic could work with any element type, but could not read domain-specific fields. A constraint limits accepted types to ones with a required capability. T extends { id: string } means T must include a string id.

This does not mean T is exactly that tiny shape. A Session with id, topic and minutes satisfies the constraint. The function can use id and still return the whole T, preserving the caller’s knowledge of topic and minutes. That is the advantage over returning only { id: string }.

Do not overconstrain. A lookup by id does not need to demand a topic or duration. Asking for only the fields the algorithm uses allows the same helper to work with books or sessions.

Constraints are checked at compile time. They do not inspect external values. A function accepting T with id:string is safe only when the calling code has established that contract. Day 6 will handle unknown inputs before they reach typed functions.

The extends keyword here describes a type constraint; it is not creating class inheritance. We are still using plain objects and ordinary array.find. Its callback returns true when a record matches, and find returns undefined when no record matches.

Predict, check, then run

Before running, write down the output and explain which information the checker knows. Click Check types first, then Check & run.

Predict, then runTypeScript
function byId<T extends { id: string }>(
  items: readonly T[],
  id: string
): T | undefined {
  return items.find(item => item.id === id);
}

const sessions = [{ id: "s1", topic: "Generics", minutes: 20 }];
const found = byId(sessions, "s1");

console.log(found?.topic ?? "Missing");
console.log(byId(sessions, "no")?.topic ?? "Missing");

Enable JavaScript to edit and type-check this example.

Check your prediction
Generics
Missing

Read a real compiler error

The constraint describes the id type as well as its name.

Find and fix the type errorTypeScript
function byId<T extends { id: string }>(
  items: readonly T[],
  id: string
): T | undefined {
  return items.find(item => item.id === id);
}

console.log(byId([{ id: 1, title: "Types" }], "1"));

Enable JavaScript to edit and type-check this example.

Check your diagnosis

Use a string id in the record if that is the domain contract. A numeric id is not automatically converted to string by the constraint.

A constraint is a minimum requirement

T extends {id:string} means the algorithm may rely on id. It does not discard the caller’s other fields. Returning T preserves those fields, which is the practical benefit over always returning the smaller id-only shape.

The constraint does not guarantee a matching id exists, that ids are unique, or that an id is nonblank. Those are separate domain and search rules. Test a missing match even when all records satisfy the structural requirement.

A constraint is a minimum requirement: traceTypeScript
function byId<T extends { id: string }>(
  values: readonly T[],
  id: string
): T | undefined {
  return values.find(value => value.id === id);
}

const found = byId([{ id: "a", title: "Types", pages: 120 }], "a");

console.log(found?.title ?? "Missing");
console.log(found?.pages ?? 0);

Enable JavaScript to edit and type-check this example.

Check your prediction
Types
120

The search body uses only id, but the return retains the full inferred record. The optional access still acknowledges a possible missing result.

A constraint is a minimum requirement: contrastTypeScript
function byId<T extends { id: string }>(
  values: readonly T[],
  id: string
): T | undefined {
  return values.find(value => value.id === id);
}

console.log(
  byId(
    [
      { id: "a", title: "First" },
      { id: "a", title: "Second" }
    ],
    "a"
  )?.title
);
console.log(byId([], "missing") === undefined);

Enable JavaScript to edit and type-check this example.

Check your prediction
First
true

Find returns the first matching record; the constraint did not enforce uniqueness. If duplicates are invalid, reject them at a validation boundary rather than silently assuming this helper proved that rule.

Try the rule on new inputs

Try richer records with a shared id field, a missing id field and a numeric id field. Explain which fail checking and which need runtime behavior tests despite satisfying the constraint.

Your turn

Exercise

Generic Constraints: practise

Write titles<T extends { title: string }>(items: readonly T[]): string[]. Use map to extract titles. Test records with title and pages, printing Types,Loops with join(”,”).

Your workspaceTypeScript
// Require title without requiring the unrelated pages property.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

The algorithm only reads title, so that is the constraint it needs.

Hint 2: a first step

Use extends with a record shape containing title:string.

Show one solution
One possible solutionTypeScript
function titles<T extends { title: string }>(items: readonly T[]): string[] {
  return items.map(item => item.title);
}

const books = [
  { title: "Types", pages: 120 },
  { title: "Loops", pages: 80 }
];

console.log(titles(books).join(","));

Enable JavaScript to edit and type-check this example.

Types,Loops

Explain the types at the function boundary and the runtime checks, if any. Compare your approach before copying this answer.

Change it and explain it

Explain why this particular titles function could also accept readonly { title: string }[] without a generic: its return does not preserve whole records. Contrast that with byId, which does preserve them. Try a missing title and read the diagnostic.

Was this article helpful?

Your response is saved on this device.