How to constrain a generic type to the keys of another type?
1) Base pattern: an object's key
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Tim" };
getProp(user, "id"); // OK
getProp(user, "age"); // Error: "age" ∉ keyof typeof userkeyof Tis the union of the string/number/symbol keys of typeT.
2) Allowing only a subset of keys
Sometimes you need not all of T's keys, but only part of them:
function getIdOrName<
T,
K extends keyof T & ("id" | "name")
>(obj: T, key: K): T[K] {
return obj[key];
}- Here
Kis the intersection of "keys of T" with a specific set"id" | "name".
3) Keys whose values match a type (filtering by value)
type KeysOfType<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never
}[keyof T];
function getNumberField<
T,
K extends KeysOfType<T, number>
>(obj: T, key: K): number {
return obj[key] as number;
}
type U = { a: number; b: string; c: number };
/* K = "a" | "c" */This kind of technique is often called value-based key filtering.
4) Required/optional/non-writable keys
Sometimes you need to restrict keys by a "qualifier" (optional/readonly):
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
}[keyof T];
type WritableKeys<T> = {
[K in keyof T]-?: { [P in K]: T[P] } extends { -readonly [P in K]: T[P] } ? K : never
}[keyof T];
function takeRequired<T, K extends RequiredKeys<T>>(obj: T, key: K) {
return obj[key];
}5) Constraining by an index signature's keys
If a type has a dictionary shape:
type Dict = Record<string, number>; // index signature
function getDict<
T extends Record<string, unknown>,
K extends keyof T
>(obj: T, key: K) {
return obj[key];
}
keyof Tfor an index signature is roughlystring | number | symbol(depends on the keys).
6) Keys of arrays and tuples
function first<T extends readonly unknown[]>(arr: T) {
const i: Extract<keyof T, number> = 0; // numeric keys only
return arr[i]; // T[number]
}For an array, keyof T also includes methods ("length", "push"). To restrict to indexes, intersect with number.
7) Keys of literal objects: as const
To get literal keys instead of "widened" ones:
const cfg = {
api: "/api",
cdn: "/cdn",
} as const;
type Cfg = typeof cfg; // { readonly api: "/api"; readonly cdn: "/cdn" }
type CfgKey = keyof Cfg; // "api" | "cdn"
function fromCfg<K extends keyof Cfg>(k: K): Cfg[K] {
return cfg[k];
}8) Keys of an enum / lookup objects
enum Role { Admin = "admin", User = "user" }
const ROLES = { Admin: "admin", User: "user" } as const;
type RoleKey = keyof typeof ROLES; // "Admin" | "User"
type RoleVal = typeof ROLES[RoleKey]; // "admin" | "user"
function allow<K extends keyof typeof ROLES>(k: K) { /* ... */ }9) Utilities for building an API: Pick/Record/Omit with constrained keys
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const out = {} as Pick<T, K>;
keys.forEach(k => (out[k] = obj[k]));
return out;
}
function mapValues<T, K extends keyof T>(
obj: T, keys: K[], fn: (v: T[K], k: K) => T[K]
): Pick<T, K> {
const res = {} as Pick<T, K>;
for (const k of keys) res[k] = fn(obj[k], k);
return res;
}10) Common pitfalls and tips
-
keyof never->never: ifTcan benever,Kis already uninferable; add a guard. -
For arrays, don't forget to filter keys down to
number, otherwise the string methods show up too. -
If the keys come from a value (an array of strings), prefer
as const:javascriptconst allowed = ["id", "name"] as const; type Allowed = typeof allowed[number]; // "id" | "name" function f<T, K extends keyof T & Allowed>(obj: T, key: K) {}
Summary
- Base:
K extends keyof T, the keys ofT. - Subset:
K extends keyof T & ("a" | "b"). - By value type:
KeysOfType<T, V>. - For arrays:
Extract<keyof T, number>. - For literals/enums:
keyof typeof X(+as const).
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.