Skip to content
TypeScript ts generics 3 min read

keyof with Generics

The real power of generics appears when you constrain one type parameter to the keys of another. Pairing keyof with a generic constraint lets you write helpers like getProp(obj, key) where the compiler guarantees the key actually exists on the object and infers the precise type of the value returned. This is the foundation of type-safe property access, object pluckers, and form utilities. This page builds the canonical getProp<T, K extends keyof T> pattern step by step and shows how indexed access types make the return type exact.

The problem getProp solves

A naive property getter that takes a string key throws away all type information: the compiler cannot check the key and the return type collapses to a broad union or any. We want the key checked and the result typed precisely.

function getPropLoose(obj: object, key: string) {
  return (obj as any)[key]; // returns any — no safety at all
}

The fix is two related type parameters: T for the object and K for a key of that object, expressed with K extends keyof T.

Constraining a key to keyof T

keyof T produces a union of T’s property names as literal types. Constraining K extends keyof T means a caller can only pass a key that truly exists, and the return type uses an indexed access type T[K] to read that property’s type.

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

const user = { id: 1, name: "Ada", active: true };

const name = getProp(user, "name");   // name: string
const id = getProp(user, "id");       // id: number
// getProp(user, "email");            // ❌ Error: "email" is not assignable to "id" | "name" | "active"

Because K is inferred as the literal "name", the return type T[K] resolves to exactly string. Pass a key that does not exist and the constraint rejects it at the call site — no runtime check needed.

Setting a property safely

The same pattern types a setter: the value parameter is T[K], so the compiler enforces that the new value matches the property’s existing type.

function setProp<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
  obj[key] = value;
}

setProp(user, "active", false); // ✅ boolean expected
// setProp(user, "id", "oops");  // ❌ Error: string not assignable to number

This is far safer than obj[key] = value with loose types, where you could write a string into a numeric field. T[K] ties the value’s type to the chosen key automatically.

Picking multiple keys

Constrain a key array with K extends keyof T to build a pick helper. The return type maps over the selected keys, producing an object containing only those properties with their original types.

function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  for (const key of keys) result[key] = obj[key];
  return result;
}

const summary = pick(user, ["id", "name"]);
// summary: { id: number; name: string }

The built-in Pick<T, K> utility describes the result type, while the K extends keyof T constraint guarantees every key in the array is valid. Selecting a non-existent key is a compile error.

ExpressionMeaningExample result
keyof TUnion of T’s keys"id" | "name" | "active"
K extends keyof TK must be a valid keyconstrains the parameter
T[K]Type of property Kstring for K = "name"
Pick<T, K>Object of selected keys{ id: number }

When K is a union of several keys, T[K] becomes a union of those property types. getProp(user, k) where k: "id" \| "name" returns string \| number. Narrow K to a single literal to get a single value type.

Best Practices

  • Use two parameters — T for the object and K extends keyof T for the key.
  • Return T[K] (indexed access) so the value type tracks the chosen key exactly.
  • Let inference fill both parameters; pass keys as literals so K stays narrow.
  • Type setter values as T[K] to prevent assigning a mismatched value.
  • Reach for built-in Pick<T, K> to describe multi-key selection results.
  • Remember a union K yields a union return type; constrain to one literal for one type.
Last updated June 29, 2026
Was this helpful?