5.6 — keyof and Generics
keyof + generics = the safest property access in TypeScript
The keyof operator is TypeScript's way of asking a simple question: "What are the valid keys for this object type?" It takes an object type and gives you back a union of all its key names as string literal types. On its own, keyof is useful. But when you combine it with generics, it becomes the most powerful tool for type-safe property access in all of TypeScript.
Imagine the Charminar — Hyderabad's iconic monument with 4 famous gates: North Gate, South Gate, East Gate, and West Gate. Each gate leads to a completely different area with different things. North Gate takes you to Laad Bazaar (shopping — string type), South Gate leads to Mecca Masjid (prayer — object type), East Gate opens to the old city markets (array of stalls), and West Gate faces the Musi river (scenic — boolean, open or not).
Now, keyof Charminar gives you exactly: "northGate" | "southGate" | "eastGate" | "westGate". These are the ONLY valid keys. If you ask for "roofGate" — ERROR! There is no roof gate on Charminar, bhai! The key maker knows EXACTLY which gates exist, no more, no less.
And here's the beautiful part — each gate leads to a SPECIFIC area with a specific type. When you go through northGate, you KNOW you'll find Laad Bazaar (a string). When you go through southGate, you KNOW it's Mecca Masjid (an object). The keyof + generics combo means: you can only use valid keys, AND you get back the RIGHT type for that key. No guessing, no any, no runtime crashes. The key maker never lies!
This is what makes keyof with generics the gold standard for building safe APIs, configuration managers, form handlers, and any code that dynamically accesses object properties. TypeScript guarantees at compile time that your key exists AND your value type is correct. That's the power we're about to unlock.
The keyof operator takes an object type and produces a union of all its property names as string literal types. This is the foundation — understand this well before we add generics into the mix. Let's start with a simple example:
type User = {
name: string;
age: number;
email: string;
};
type UserKey = keyof User;
// "name" | "age" | "email"
UserKey is not string. It is specifically "name" | "age" | "email" — a union of exact string literal types. TypeScript is not approximating here; it knows the precise set of keys. This works with interfaces too:
interface Config {
host: string;
port: number;
debug: boolean;
}
type ConfigKey = keyof Config;
// "host" | "port" | "debug"
Notice how keyof returns string literal unions for objects with string keys. But what if your object has number keys or index signatures? For objects with number keys, keyof will include number in the union. For objects with an index signature like { [key: string]: any }, keyof will include string. And there's a special case: keyof any evaluates to string | number | symbol — the complete set of all possible key types in JavaScript.
type AllKeys = keyof any;
// string | number | symbol
type ArrKeys = keyof string[];
// number | typeof methods...
Now let's try using keyof in a function:
function getKey<T>(
obj: T,
key: keyof T
) {
return obj[key];
}
This looks good — it constrains key to be a valid key of T. But there's a problem: the return type is T[keyof T], which is the union of ALL value types in T. If T has properties of type string, number, and boolean, the return type would be string | number | boolean — even if you pass the specific key "name" which should return string. This is not precise enough! We need generics on the KEY itself to preserve the exact return type. That's exactly what we'll cover in the next section — the real power of keyof with generics.
Here is the killer combo — the pattern that makes TypeScript developers fall in love with keyof. When you combine keyof with a second generic parameter, you get precise, type-safe property access that knows both the key AND the exact value type at that key:
function getProperty<
T,
K extends keyof T
>(obj: T, key: K): T[K] {
return obj[key];
}
Let's break this down piece by piece. T is the object type — it captures the full shape of the object you pass in. K extends keyof T means K must be one of T's keys — it's constrained to only valid property names. And T[K] is an indexed access type — it means "the type of the property at key K on type T". This is the magic ingredient that gives you the precise return type!
const user = {
name: "Imran",
age: 25,
active: true
};
// T = user type, K = "name"
getProperty(user, "name");
// Return type: string ✓
// T = user type, K = "age"
getProperty(user, "age");
// Return type: number ✓
// ERROR! "email" not in user
getProperty(user, "email");
See what happened? When you call getProperty(user, "name"), TypeScript infers T as the type of user and K as the literal type "name". The return type becomes T["name"] which is string. When you call getProperty(user, "age"), K is "age" and the return type is number. And if you try getProperty(user, "email") — TypeScript immediately errors because "email" is not a valid key! No more typos slipping through to production!
Now let's add the setter — this is where it gets even more powerful:
function setProperty<
T,
K extends keyof T
>(
obj: T,
key: K,
value: T[K]
): void {
obj[key] = value;
}
// ✓ This works — age is number
setProperty(user, "age", 30);
// ERROR! age expects number,
// not string
setProperty(
user, "age", "twenty-five"
);
Now even the value you're setting is type-checked! T[K] appears both as the return type of the getter AND as the type of the value parameter in the setter. TypeScript ensures that the value you pass matches the type of the property at that key. setProperty(user, "age", "twenty-five") fails because age is number, not string. This is the gold standard of type safety — both the key AND the value are fully validated at compile time!
This pattern is the backbone of safe API wrappers, configuration objects, form handlers, state managers, and any code that needs to dynamically access object properties. Once you internalize K extends keyof T and T[K], you'll see these patterns everywhere in well-typed TypeScript codebases. It's like having a Charminar gatekeeper who not only checks you're at a valid gate but also tells you exactly what's on the other side!
Now let's explore more sophisticated uses of keyof that you'll encounter in real TypeScript codebases. These patterns form the foundation of many utility types and advanced type-level programming techniques.
Pattern 1: Filtering properties by type. What if you want to extract only the properties whose values are strings? Combine keyof with mapped types and conditional types:
// Extract only string-valued props
type StringProps<T> = {
[K in keyof T as
T[K] extends string
? K : never
]: T[K];
};
type User = {
name: string;
age: number;
email: string;
active: boolean;
};
type UserStrings =
StringProps<User>;
// { name: string; email: string }
The as clause in the mapped type acts as a filter — when T[K] extends string, the key is kept; otherwise it's replaced with never, which removes it from the final type. We'll dive deeper into mapped types in Stage 7.
Pattern 2: Extracting required keys. You can identify which keys of a type are required (not optional) using a clever conditional check:
type RequiredKeys<T> = {
[K in keyof T]-?:
{} extends Pick<T, K>
? never : K
}[keyof T];
type Config = {
host: string;
port?: number;
debug?: boolean;
};
type Req = RequiredKeys<Config>;
// "host"
The trick: {} extends Pick<T, K> is true only when the property is optional — an empty object can satisfy an optional property. If it's required, the condition fails and we get the key name instead of never.
Pattern 3: The Object.keys gotcha. At runtime, Object.keys(obj) returns the actual keys of an object. But TypeScript types it as string[], not (keyof T)[]. This is a deliberate design decision — TypeScript cannot guarantee that the runtime object has exactly the keys its type declares. To safely iterate with typed keys:
const obj = { a: 1, b: 2, c: 3 };
Object.keys(obj);
// Type: string[]
// Not ("a"|"b"|"c")[] !
// Safe cast — use with caution!
const keys =
Object.keys(obj) as (
keyof typeof obj
)[];
keys.forEach(k => {
// k is "a" | "b" | "c"
console.log(obj[k]);
});
Pattern 4: keyof with classes. You can use keyof to create type-safe class methods that access properties by name:
class Store<T> {
private data: T;
constructor(data: T) {
this.data = data;
}
get(key: keyof T): T[keyof T] {
return this.data[key];
}
}
const s = new Store({
x: "chai",
y: 42,
});
s.get("x"); // string | number
Note that without a second generic for the key, get returns T[keyof T] — the union of all value types. For precise returns, add K extends keyof T to the method signature.
Pattern 5: keyof as the foundation of utility types. All the built-in utility types you use daily — Partial<T>, Required<T>, Readonly<T>, Pick<T, K>, Omit<T, K> — are built on keyof and mapped types. For example, Partial<T> is essentially { [K in keyof T]?: T[K] } and Pick<T, K extends keyof T> is { [P in K]: T[P] }. Understanding keyof means understanding how these utilities work under the hood. We'll build our own utility types from scratch in Stage 7!
Here's your complete cheatsheet for keyof and generics — the Charminar key maker's handbook. Pin this to your wall!
Basic keyof:
type K = keyof T;
// Union of T's key names
// e.g. keyof {a: string; b: number}
// = "a" | "b"
Indexed Access Type:
type V = T[K];
// Type of property K on T
// e.g. {a: string}["a"] = string
Safe Getter (The Gold Standard):
function get<T, K extends keyof T>(
obj: T, key: K
): T[K] {
return obj[key];
}
// Key is validated, return is
// the EXACT type at that key
Safe Setter (Double Protection):
function set<
T,
K extends keyof T
>(
obj: T,
key: K,
val: T[K]
): void {
obj[key] = val;
}
// Both key AND value checked!
Special Values:
keyof any
// = string | number | symbol
// Object.keys returns string[]
// NOT (keyof T)[]
Object.keys(obj); // string[]
The Five Golden Rules of keyof:
- Rule 1:
keyofgives you ONLY valid key names — no typos possible. If the object hasname, you can only use"name", not"nme"or"nama". - Rule 2:
K extends keyof Tconstrains K to be one of T's valid keys. This is the constraint that makes the whole pattern work — without it, you lose type safety. - Rule 3:
T[K]gives you the exact value type for that key. Not a union of all values — the specific type at that specific key. This is what makes the getter return precisely the right type. - Rule 4: The
keyof+ generics combo is the safest way to access dynamic properties. It's like having a Charminar gatekeeper who knows every gate, checks your ticket, and tells you exactly what's on the other side. - Rule 5:
keyofis the foundation of ALL utility types —Partial,Required,Readonly,Pick,Omit, and more. Masterkeyofand you understand how half of TypeScript's type system works.
The golden rule of this chapter: "keyof is the Charminar key maker — it knows exactly which gates exist, and each gate leads to the right place. Use it with generics, and you'll never access a wrong property again, bhai!"
Key Points
- keyof T produces a union of all key names of T as string literal types
- K extends keyof T constrains K to be a valid key of T
- T[K] is an indexed access type — the exact type of property K on T
- keyof any equals string | number | symbol
- Object.keys() returns string[], not (keyof T)[] — use type assertion with caution
- keyof + generics = the safest dynamic property access pattern in TypeScript
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login