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.
| Expression | Meaning | Example result |
|---|---|---|
keyof T | Union of T’s keys | "id" | "name" | "active" |
K extends keyof T | K must be a valid key | constrains the parameter |
T[K] | Type of property K | string for K = "name" |
Pick<T, K> | Object of selected keys | { id: number } |
When
Kis a union of several keys,T[K]becomes a union of those property types.getProp(user, k)wherek: "id" \| "name"returnsstring \| number. NarrowKto a single literal to get a single value type.
Best Practices
- Use two parameters —
Tfor the object andK extends keyof Tfor 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
Kstays 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
Kyields a union return type; constrain to one literal for one type.