Skip to main content
CodeOath
← All posts

TypeScript63 min total · 21 parts

TypeScript Fundamentals: Types, Interfaces, Generics, and Why It Catches Bugs Before Runtime

Part 12 of 21 · ~2 min

Generic Constraints and keyof/typeof

<T> on its own promises nothing about what T actually is, which means the function body can't lean on it having any particular field — item.length inside a firstMatch-style function would be a compile error, because some T somewhere might not have a .length at all. extends is how you put a floor under T, without pinning it to one exact type:

function scoreLength<T extends { length: number }>(item: T): number {
  return item.length; // legal now — every T accepted here is guaranteed to have .length
}

scoreLength("Priya Shah");       // strings have .length
scoreLength([8841, 8842, 8843]); // arrays have .length
scoreLength(8841);               // Error — a bare number doesn't

keyof is a related idea, but it operates on a specific type's actual property names rather than a structural requirement: it turns Applicant's keys into the literal union "id" | "name" | "email" | "stage". That's the exact ingredient needed for a generic, type-checked "read one field by name" helper — something the queue needs for a small audit log that records which field on an applicant record just changed:

function getField<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const applicant: Applicant = { id: 8841, name: "Priya Shah", email: "priya@example.com", stage: "screening" };
getField(applicant, "name");  // string — and only real keys of Applicant type-check here
getField(applicant, "phone"); // Error — "phone" was never a key of Applicant

K extends keyof T is the part doing the actual restricting — without it, key would accept any string at all, real field or not. T[K], an indexed access type, is what lets the return type track exactly which field got asked for: getField(applicant, "name") returns string, specifically, rather than the union of every field's type across the whole interface.

There's also a typeof that lives in type position rather than expression position — a different tool from the runtime typeof a few chapters back, despite the shared keyword. This one reads the type of an already-existing value, which is handy for deriving a type from a real object instead of maintaining a hand-written copy of its shape that can quietly go stale:

const defaultQueueFilters = { stage: "screening" as const, assignedOnly: true };
type QueueFilters = typeof defaultQueueFilters; // { stage: "screening"; assignedOnly: boolean }