TypeScript keyof Operator
The keyof operator takes an object type and produces a union of the literal names of its properties. It lets you write functions and types that stay perfectly in sync with the shape of another type, so that if you rename or remove a property, TypeScript immediately flags every place that referenced it. It’s one of the core building blocks behind generic, reusable, and still fully type-safe utility code.
Because keyof operates purely at the type level, it costs nothing at runtime — there is no array of strings hiding behind it until you explicitly ask for one. Understanding that distinction is the key to using it correctly.
Overview: How keyof Works
keyof is a type operator, similar in spirit to JavaScript’s typeof but operating entirely in the type system. Given an object type T, keyof T produces a union type made up of the literal names of every property in T. For example, if T has properties name and age, then keyof T is the type "name" | "age" — not an array, not a runtime value, just a type made of string literal types joined by unions.
This makes keyof extremely useful anywhere you need a type that is guaranteed to track another type’s property names. The most common use is constraining a generic parameter so a function can only be called with a key that actually exists on the object it receives:
function get<T, K extends keyof T>(obj: T, key: K): T[K]
Here K extends keyof T means “K must be one of T’s property names,” and T[K] (an indexed access type) resolves to the exact type of that property. Together, keyof and indexed access types give you a fully type-checked way to look up properties dynamically, without resorting to any.
keyof also interacts with index signatures. If a type has a string index signature like [key: string]: number, then keyof on that type produces string | number, not just string. This is because JavaScript object keys are always coerced to strings, and TypeScript models numeric-looking keys (like array indices) as being valid for a string index too, so it includes number in the union as a convenience for indexing with numbers.
keyof pairs naturally with mapped types (covered in depth in another lesson), which use the syntax { [K in keyof T]: ... } to build a new type by iterating over every key of an existing one. This is exactly how built-in utility types like Partial<T>, Readonly<T>, and Record<K, V> are implemented internally.
Syntax
interface Example {
a: string;
b: number;
}
type ExampleKeys = keyof Example; // "a" | "b"
keyof— the operator itself; always followed by a type, never a value or variable.Example— any object type: aninterface, atypealias, or even aclass.- Result — a union of string (and possibly
numberorsymbol) literal types, one for each property.
With an index signature the result changes shape:
interface StringMap {
[key: string]: number;
}
type StringMapKeys = keyof StringMap; // string | number
There is no runtime equivalent of StringMapKeys — you cannot iterate over it directly. To get the actual key strings at runtime you still need Object.keys(), cast to the right type, as you’ll see below.
Examples
Example 1: A type-safe property getter
interface Person {
name: string;
age: number;
email: string;
}
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person: Person = { name: "Ada", age: 30, email: "ada@example.com" };
const name = getProperty(person, "name");
const age = getProperty(person, "age");
console.log(name, age);
Output:
Ada 30
The constraint K extends keyof T means TypeScript rejects any key that isn’t actually a property of Person at the call site — try calling getProperty(person, "nickname") and the compiler stops you before the code ever runs. The return type T[K] is inferred precisely: name comes back as string, age as number, with no casting required.
Example 2: Comparing objects with keyof
interface Settings {
theme: string;
fontSize: number;
notifications: boolean;
}
function changedKeys<T extends object>(before: T, after: T): (keyof T)[] {
return (Object.keys(before) as Array<keyof T>).filter(
(key) => before[key] !== after[key]
);
}
const oldSettings: Settings = { theme: "light", fontSize: 14, notifications: true };
const newSettings: Settings = { theme: "dark", fontSize: 14, notifications: false };
const diff = changedKeys(oldSettings, newSettings);
console.log(diff);
Output:
[ 'theme', 'notifications' ]
This is a very common real-world pattern: a generic function that works on any object shape, using keyof T both as the return element type and, after an explicit cast, to safely iterate over Object.keys(). Notice the cast Object.keys(before) as Array<keyof T> — this is necessary because Object.keys() always returns string[] at the type level (explained further in Under the Hood).
Example 3: Extracting a column of values (“pluck”)
interface Employee {
id: number;
name: string;
department: string;
salary: number;
}
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
const employees: Employee[] = [
{ id: 1, name: "Grace", department: "Engineering", salary: 95000 },
{ id: 2, name: "Alan", department: "Research", salary: 88000 },
];
const names = pluck(employees, "name");
const salaries = pluck(employees, "salary");
console.log(names);
console.log(salaries);
Output:
[ 'Grace', 'Alan' ]
[ 95000, 88000 ]
pluck is fully generic and fully typed: calling pluck(employees, "salary") returns number[], while pluck(employees, "name") returns string[], both inferred automatically from T[K]. This is the same idea behind array utility libraries’ typed pluck/mapValues helpers.
Under the Hood
When the compiler sees keyof T, it looks at every declared property of T (including inherited ones from extends) and builds a union of literal types from their names. This happens entirely during type-checking. Once compilation finishes, all type information is erased — the emitted JavaScript for every example above contains no trace of keyof, K extends keyof T, or T[K]. Erasure is why you cannot write const keys = keyof Config and expect an array: there is nothing left at runtime to assign.
This erasure is also why Object.keys() is typed to return plain string[] rather than (keyof T)[]. TypeScript can’t guarantee that an object passed at runtime has exactly the properties of its declared type and nothing else — extra properties can slip in through wider types, subclassing, or spreading. So the built-in library types are conservative, and the common, accepted pattern is to assert the narrower type yourself with as Array<keyof T> once you’re confident the object genuinely only has those keys (as in Example 2 and the fix below).
For an index signature type, the compiler adds number to the union because JavaScript itself does this coercion: obj[0] and obj["0"] access the same property. TypeScript’s keyof reflects that reality rather than inventing a stricter rule that doesn’t match the language.
Common Mistakes
Mistake 1: Trying to use keyof as a runtime value
keyof only exists in type positions. Writing it where a value is expected is a compile error, not a runtime bug — but it’s an easy slip if you’re thinking of it like Object.keys().
interface Config {
host: string;
port: number;
}
const keys = keyof Config;
console.log(keys);
tsc reports: 'keyof' only refers to a type, but is being used as a value here. The fix is to either annotate a variable’s type with keyof, or reach for Object.keys() plus a cast if you need the actual key strings at runtime:
interface Config {
host: string;
port: number;
}
const keys: (keyof Config)[] = ["host", "port"];
console.log(keys);
Output:
[ 'host', 'port' ]
Mistake 2: Indexing with a plain string from Object.keys()
Because Object.keys() returns string[], using its result directly to index a specifically-typed object fails under strict mode’s noImplicitAny check:
interface Settings {
theme: string;
fontSize: number;
}
function logSettings(settings: Settings): void {
const keys = Object.keys(settings);
keys.forEach((key) => {
console.log(settings[key]);
});
}
tsc reports: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Settings'. The fix is the cast pattern shown earlier — tell the compiler that, in this specific spot, you know the strings really are keys of Settings:
interface Settings {
theme: string;
fontSize: number;
}
function logSettings(settings: Settings): void {
(Object.keys(settings) as Array<keyof Settings>).forEach((key) => {
console.log(settings[key]);
});
}
logSettings({ theme: "dark", fontSize: 14 });
Output:
dark
14
Best Practices
- Prefer
K extends keyof Tgenerics overanyor hardcoded string unions whenever a function needs to accept “a property name of this object” — it keeps callers and refactors type-checked automatically. - Only cast
Object.keys(obj)toArray<keyof T>when you’re confidentobjhas exactly the shape ofT, with no extra or missing properties — the cast bypasses a real (if conservative) safety check. - Combine
keyofwith indexed access types (T[K]) rather than duplicating a value’s type by hand; if the source type changes, your derived type updates automatically. - Remember that a type with a string index signature yields
string | numberfromkeyof— don’t be surprised whennumbershows up in a union you expected to be pure strings. - Use
keyof typeof someObject(pairing withtypeof) when you want the keys of a runtime object literal or enum-like object as a type, instead of redeclaring an interface.
Practice Exercises
Exercise 1: Write a generic function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void that mutates obj[key] to value. Call it on an object with at least three differently-typed properties and confirm each call type-checks only with a matching value type.
Exercise 2: Given an interface Book { title: string; author: string; pages: number; }, write a type alias BookKeys using keyof, then write a function hasKey(obj: Book, key: string): key is BookKeys style check (a type guard) that verifies a plain string is actually a valid key of Book before using it to index.
Exercise 3: Define type Flags = { [key: string]: boolean }; and write down (as a comment) what keyof Flags evaluates to and why. Then write a function that accepts a Flags object and a keyof Flags key and toggles that flag’s boolean value.
Summary
keyof Tproduces a union of the literal property-name types ofT— it exists only in the type system and is erased at runtime.- It’s most often used to constrain a generic parameter (
K extends keyof T) so a function can only accept valid property names of the type it’s given. - Combined with indexed access types (
T[K]),keyoflets you write generic getters, setters, and diffing utilities that stay in sync with a type’s shape automatically. - A type with a string index signature produces
string | numberfromkeyof, reflecting how JavaScript treats numeric and string keys as interchangeable. Object.keys()always returns plainstring[]at the type level; cast toArray<keyof T>only when you’re sure the object matchesTexactly.keyofcan never be used as a runtime value directly — attemptingconst x = keyof SomeTypeis always a compile error.
