Stockio

TypeScript Generics Explained With keyof Examples

Learn how generic constraints, keyof, and indexed access preserve relationships between inputs and outputs without resorting to broad unions or unsafe assertions.

By Daniel Kim

React Developer ·

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 error

Here, 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 T with T[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.
Featured sponsor

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

Keep reading

Related articles

More in JavaScript & Frameworks
JavaScript & Frameworks

10 Best JavaScript Charting Libraries

Compare ten JavaScript chart libraries for dashboards, reports, and interactive data stories. See where each excels, what it costs, and how to choose based on your app and team.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

8 Best React State Management Libraries

Find a React state approach that fits your app, from built-in hooks to dedicated stores, with clear trade-offs around data flow, debugging, async work, and learning effort.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

12 Best and Most Popular JavaScript Libraries

Compare twelve widely used JavaScript libraries for interfaces, data, testing, and everyday development, with clear strengths, trade-offs, and advice for choosing only what your project needs.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

localStorage vs sessionStorage vs IndexedDB Compared

Choose browser storage by data lifetime, size and access pattern. Compare synchronous Web Storage with IndexedDB transactions, migrations, quotas and failure handling.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

React Server Components Explained for Developers

Learn what React Server Components execute, what crosses the client boundary, and how to structure data access, interactive widgets, and mutations without leaking private data.

Includes: JSON formatterDaniel KimDaniel Kim
JavaScript & Frameworks

useMemo vs useCallback vs React.memo: Fewer Rerenders

Learn when React memoization helps, how prop identity affects rerenders, and why profiling and state placement should come before useMemo and useCallback.

Includes: JSON formatterDaniel KimDaniel Kim