The keyof Operator
The keyof operator takes a type and produces a union of its known property names as string, number, or symbol literal types. It is one of TypeScript's most fundamental type-level tools — the basis of safe property access, mapped types, and many built-in utility types.
Basic Usage
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
// keyof produces a union of the property names
type UserKeys = keyof User;
// "id" | "name" | "email" | "role"
const key1: UserKeys = 'name'; // ✅
const key2: UserKeys = 'email'; // ✅
// const key3: UserKeys = 'age'; // ❌ not a key of Userkeyof operates on types, not values. To get the keys of an object at runtime, use Object.keys(obj). At the type level, use keyof typeof obj.keyof with Index Signatures
// String index signature — keyof produces string | number
interface StringDict {
[key: string]: unknown;
}
type StringDictKeys = keyof StringDict; // string | number
// (number is included because JS coerces numeric keys to strings)
// Number index signature
interface NumberDict {
[key: number]: string;
}
type NumberDictKeys = keyof NumberDict; // number
// Mixed (both signatures)
interface MixedDict {
[key: string]: unknown;
length: number; // explicit property alongside index sig
}
type MixedKeys = keyof MixedDict; // string | numberSafe Property Access with keyof
Combining keyof with a generic type parameter is the canonical way to write a safe property getter — one that is both flexible and compile-time verified.
// Generic safe getter
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
const name = get(user, 'name'); // string
const id = get(user, 'id'); // number
// get(user, 'password'); // ❌ 'password' is not assignable to keyof typeof user
// Generic safe setter
function set<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}
set(user, 'name', 'Bob'); // ✅
// set(user, 'name', 42); // ❌ number is not assignable to stringkeyof with typeof
typeof (the type-level operator) extracts the type of a value. Combining keyof typeof is the standard way to get the keys of a runtime object as a type.
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
debug: false,
};
// Extract keys of the runtime object as a type
type ConfigKey = keyof typeof config;
// "apiUrl" | "timeout" | "retries" | "debug"
function getConfig(key: ConfigKey): typeof config[ConfigKey] {
return config[key];
}
const url = getConfig('apiUrl'); // string
const t = getConfig('timeout'); // number
// getConfig('unknown'); // ❌keyof in Mapped Types
Mapped types iterate over keyof T to transform every property. This is how built-in utilities like Partial<T>, Required<T>, and Readonly<T> are implemented.
// Readonly<T> — the standard library implementation
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Partial<T>
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Nullable<T> — make every property nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Stringify<T> — convert all values to string
type Stringify<T> = {
[K in keyof T]: string;
};
interface Point { x: number; y: number }
type ReadonlyPoint = MyReadonly<Point>; // { readonly x: number; readonly y: number }
type PartialPoint = MyPartial<Point>; // { x?: number; y?: number }
type NullablePoint = Nullable<Point>; // { x: number | null; y: number | null }keyof with Conditional Types
// Pick only the keys whose values extend a given type
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
interface Form {
name: string;
age: number;
email: string;
active: boolean;
score: number;
}
type StringKeys = KeysOfType<Form, string>; // "name" | "email"
type NumberKeys = KeysOfType<Form, number>; // "age" | "score"
type BooleanKeys = KeysOfType<Form, boolean>; // "active"
// Use it to build a type-safe form validator
function validateStringFields<T>(
obj: T,
fields: KeysOfType<T, string>[]
): void {
fields.forEach(field => {
const value = obj[field];
if (typeof value !== 'string' || value.trim() === '') {
console.warn(`Field ${String(field)} must be a non-empty string`);
}
});
}{ [K in keyof T]: ... }[keyof T] is called a distributive mapped type — iterating and immediately indexing produces a union of the resulting types.Excluding Keys with keyof
// Omit<T, K> — the standard library implementation
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Or using a mapped type directly
type OmitDirect<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
interface User {
id: number;
name: string;
email: string;
password: string;
}
type PublicUser = MyOmit<User, 'password'>;
// { id: number; name: string; email: string }
// Rename keys
type RenameKey<T, K extends keyof T, N extends string> = {
[P in keyof T as P extends K ? N : P]: T[P];
};
type UserWithUsername = RenameKey<User, 'name', 'username'>;
// { id: number; username: string; email: string; password: string }keyof and Enums
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
// keyof typeof enum gives the enum member names
type DirectionKey = keyof typeof Direction;
// "Up" | "Down" | "Left" | "Right"
// typeof enum gives the value types
type DirectionValue = (typeof Direction)[DirectionKey];
// "UP" | "DOWN" | "LEFT" | "RIGHT"
function move(direction: DirectionKey): void {
const value = Direction[direction]; // DirectionValue
console.log(`Moving ${value}`);
}
move('Up'); // Moving UP
move('Right'); // Moving RIGHT
// move('North'); // ❌Real-World Example: Type-Safe Event System
// Define all application events and their payloads
interface AppEvents {
userCreated: { id: number; name: string; email: string };
userDeleted: { id: number };
postPublished: { postId: number; authorId: number; title: string };
errorOccurred: { code: string; message: string; stack?: string };
}
type EventName = keyof AppEvents;
class TypedEventBus {
private handlers = new Map<EventName, Set<(payload: unknown) => void>>();
on<K extends EventName>(
event: K,
handler: (payload: AppEvents[K]) => void
): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
const set = this.handlers.get(event)!;
set.add(handler as (payload: unknown) => void);
return () => set.delete(handler as (payload: unknown) => void);
}
emit<K extends EventName>(event: K, payload: AppEvents[K]): void {
this.handlers.get(event)?.forEach(handler => handler(payload));
}
}
const bus = new TypedEventBus();
// Fully type-safe: payload is inferred from the event name
bus.on('userCreated', ({ id, name, email }) => {
console.log(`New user ${name} (${id}): ${email}`);
});
bus.emit('userCreated', { id: 1, name: 'Alice', email: 'alice@example.com' });
// bus.emit('userCreated', { id: 1 }); // ❌ Missing name and emailkeyof on Intersection and Union Types
interface A { a: string; b: number }
interface B { b: number; c: boolean }
// keyof intersection = union of keys
type ABKeys = keyof (A & B); // "a" | "b" | "c"
// keyof union = intersection of keys (only shared keys)
type AorBKeys = keyof (A | B); // "b" (the only key present in BOTH)
// This makes sense: if you have A | B you can only safely access
// keys that exist on BOTH brancheskeyof (A | B) gives the intersection of keys (shared keys only) — not the union. This surprises many developers. If you need all possible keys, use keyof A | keyof B.keyof for Validation and Configuration
// Restrict sort field to valid entity keys
interface SortOptions<T> {
field: keyof T;
direction: 'asc' | 'desc';
}
function sortEntities<T>(items: T[], options: SortOptions<T>): T[] {
return [...items].sort((a, b) => {
const av = a[options.field];
const bv = b[options.field];
const cmp = av < bv ? -1 : av > bv ? 1 : 0;
return options.direction === 'asc' ? cmp : -cmp;
});
}
interface Product { id: number; name: string; price: number }
const products: Product[] = [
{ id: 3, name: 'Keyboard', price: 79 },
{ id: 1, name: 'Mouse', price: 29 },
{ id: 2, name: 'Monitor', price: 299 },
];
const byPrice = sortEntities(products, { field: 'price', direction: 'asc' });
// byPrice[0].price === 29
// sortEntities(products, { field: 'weight', direction: 'asc' }); // ❌Building a Type-Safe Configuration Updater
// Only allow updating specific known fields
interface AppSettings {
theme: 'light' | 'dark';
language: string;
pageSize: number;
notifications: boolean;
}
type SettingKey = keyof AppSettings;
class SettingsManager {
private settings: AppSettings = {
theme: 'light',
language: 'en',
pageSize: 20,
notifications: true,
};
get<K extends SettingKey>(key: K): AppSettings[K] {
return this.settings[key];
}
set<K extends SettingKey>(key: K, value: AppSettings[K]): void {
this.settings[key] = value;
}
update(partial: Partial<AppSettings>): void {
Object.assign(this.settings, partial);
}
reset(...keys: SettingKey[]): void {
// reset to defaults omitted for brevity
console.log(`Resetting: ${keys.join(', ')}`);
}
}
const mgr = new SettingsManager();
mgr.set('theme', 'dark'); // ✅
mgr.set('pageSize', 50); // ✅
// mgr.set('theme', 'blue'); // ❌ 'blue' not in 'light' | 'dark'
// mgr.set('fontSize', 14); // ❌ 'fontSize' not a key of AppSettings
const theme = mgr.get('theme'); // 'light' | 'dark'
const size = mgr.get('pageSize'); // numberQuick Reference
keyof T — produces a union of all known property name types of T
keyof typeof obj — get keys of a runtime object as a type
K extends keyof T — constrain K to valid keys of T
T[keyof T] — get a union of all value types of T
keyof (A & B) gives the union of keys; keyof (A | B) gives the intersection
Use keyof in mapped types to transform every property of a type
Combine keyof with SortOptions<T> or similar to build type-safe configuration APIs
keyof — one of the most frequently used type-level operators. Next: the typeof type operator, which lets you extract types from runtime values.