On this page
A reusable helper should preserve information, not erase it. If a function reads an object's title property, callers should receive that property's type rather than a union of every possible field. TypeScript generics describe these relationships and let the compiler carry useful knowledge through otherwise ordinary JavaScript functions.
Find the relationship before adding a type parameter
A generic type parameter represents a relationship between values or types. It is not a badge that makes a function reusable. Start with a concrete task: selecting a field from a configuration object while preserving its return type. That task has two related inputs, the object and a key belonging to it.
Use the JSON formatter to inspect a small configuration fixture containing a string title, numeric page size, and boolean enabled flag. Formatting makes the field distinctions easy to review. It cannot infer whether a generic implementation is sound, so keep compiler examples beside the fixture.
The key design goal is information preservation. If every caller receives unknown, the helper may be safe but inconvenient. If every caller receives any, it has abandoned checking. A useful generic keeps the relationship that callers already know.
Connect object keys to returned values
Use one type parameter for the object and another constrained to its keys. An indexed access type describes the selected property's value. The generics handbook explains how inference chooses type arguments from actual calls.
function getField<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
const settings = { title: "Library", pageSize: 24, enabled: true };
const title = getField(settings, "title");
const size = getField(settings, "pageSize");
// getField(settings, "missing"); // compile-time errorHere, title is a string and size is a number. Returning T[keyof T] instead would produce a broader union because it describes every property, not the property selected by this call. This is why the second type parameter matters.
Avoid adding explicit type arguments at every call site. Let type inference do the routine work. Explicit arguments are useful when inference lacks enough evidence, but constant manual annotation can signal that the abstraction does not match how people actually use it.
Understand what a constraint promises
A constraint says which operations the function may perform. It does not promise that a generic value is exactly the constraint. If T extends { id: string }, the implementation may read an id, but T might also require a label, permissions, or other fields.
Consequently, a function cannot manufacture an arbitrary T by returning only { id: "new" }. That object satisfies the constraint, not every possible subtype. When construction is necessary, accept a factory supplied by the caller or return the smaller concrete type honestly.
function indexById<T extends { id: string }>(items: readonly T[]) {
const result = new Map<string, T>();
for (const item of items) result.set(item.id, item);
return result;
}This helper preserves each item's additional properties and accepts a readonly input because it never mutates the array. Document that duplicate identifiers keep the last item. That runtime policy is independent of the generic constraint, but it is just as important to callers.
Read the keyof documentation before assuming every key is a string. JavaScript objects can also have symbol keys, and number-like access introduces additional subtleties. Constrain keys deliberately when an API truly requires strings.
Keep runtime uncertainty visible
Static key checking does not make every lookup successful. A dictionary can lack a requested entry at runtime, and Map.get returns a possibly undefined result. Model absence rather than hiding it behind an assertion.
Enable noUncheckedIndexedAccess when you want unchecked indexed reads to include undefined where appropriate. A helper operating on a declared object's known required key differs from one operating on arbitrary strings. Do not collapse those two cases into a single signature just to shorten the API.
Be careful with Object.keys. Its return type is not generally a precise array of keyof T, because runtime objects may contain more properties than their static type exposes. A blanket assertion can be acceptable inside a tightly controlled abstraction, but it needs a real invariant, not a comment claiming the compiler is confused.
This is the boundary between static relationships and runtime guarantees. Generics cannot validate a configuration file, prove an array is nonempty, or ensure identifiers are unique. They can preserve those properties after another part of the program establishes them.
Review inference through caller examples
Test a generic API from the consumer's perspective. Include a literal key, a union of keys, an optional property, and a readonly object. Ask what each result should be before hovering over the inferred type. A union key should normally produce a union result, since either property may be chosen.
Use compile-time regression tests for expected errors. A type test can check that a nonexistent key fails and that selecting pageSize does not return a string. Runtime tests should separately cover duplicate identifiers, empty arrays, and object identity.
The indexed access types guide is a useful reference when signatures become difficult to read. Prefer a named intermediate alias over several nested bracket expressions if that makes the relationship clearer to teammates.
Publish usage examples with your other language guides, and keep the function's operational contract in the development documentation. Explain mutation, missing values, and duplicate handling explicitly. A precise signature helps callers write correct code, but a signature alone rarely captures all the decisions a working helper makes.
Check optional properties and union keys
Extend the settings example with an optional subtitle property. Reading title should still produce string, while reading subtitle should produce string or undefined under strict null checking. This is not a weakness in the helper: it accurately carries the original object's uncertainty to the caller. Decide where a fallback belongs instead of changing the helper to pretend every property exists.
Next choose between title and pageSize using a runtime condition. The key now has a union type, so the returned value should be string or number. A caller that needs string methods must narrow the result or retain a more explicit relationship between the chosen key and its value. A generic function cannot know which branch a runtime condition will select.
Finally, compare a literal settings object with a broadly annotated dictionary. Once the caller has widened its object to a string-keyed record of string-or-number values, the helper cannot recover the original field-specific types. This caller-side precision matters as much as the function signature. Keep fixtures for both forms so documentation does not promise inference that depends on information already erased upstream. Prefer the simplest signature that passes these realistic cases; adding overloads or assertions without a demonstrated consumer need usually makes the abstraction harder to trust.
Key takeaways
- Add generics to preserve a specific relationship.
- Use
K extends keyof TwithT[K]for keyed reads. - Treat constraints as minimum capabilities, not exact shapes.
- Preserve missing-value behavior instead of asserting it away.
- Test inference and runtime policy independently.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor