TypeScriptkeyof Operator

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

TS
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 User
Note
keyof 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

TS
// 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 | number
Safe 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.

TS
// 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 string
keyof 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.

TS
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.

TS
// 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

TS
// 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`);
    }
  });
}
Tip
The pattern { [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

TS
// 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

TS
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

TS
// 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 email
keyof on Intersection and Union Types

TS
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 branches
Warning
keyof (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

TS
// 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

TS
// 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'); // number
Quick 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

Success
You now understand keyof — one of the most frequently used type-level operators. Next: the typeof type operator, which lets you extract types from runtime values.