keyof & typeof Operators

Difficulty: Intermediate

Question

Explain the keyof and typeof operators in TypeScript. How do they work together for type-safe property access?

Answer

keyof and typeof are two type-level operators that are often used together:

'keyof T' produces a union of all known public property names (keys) of type T as string literal types.

'typeof value' (in a type context) extracts the TypeScript type of a runtime value. This is different from JavaScript's runtime typeof operator.

Combined pattern: typeof value gives you the type, then keyof typeof value gives you its keys. This is essential for deriving types from runtime objects like config files, constants, and enum-like objects.

Code examples

keyof Operator

interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

// keyof produces union of keys
type UserKeys = keyof User;
// 'id' | 'name' | 'email' | 'isActive'

// Type-safe property access
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = { id: 1, name: 'Alice', email: 'a@b.com', isActive: true };

const name = getProperty(user, 'name');    // type: string
const id = getProperty(user, 'id');        // type: number
// getProperty(user, 'foo');                // Error: 'foo' not in keyof User

// keyof with index signatures
interface StringMap {
  [key: string]: unknown;
}
type StringMapKeys = keyof StringMap; // string | number
// (number because JS converts numeric keys to strings)

keyof extracts property names as a union of string literals. Combined with generics, it enables type-safe property access where the return type matches the specific key.

typeof Operator (Type Context)

// typeof extracts the type of a runtime value
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
  debug: false,
};

type Config = typeof config;
// { apiUrl: string; timeout: number; retries: number; debug: boolean }

// typeof + keyof together
type ConfigKey = keyof typeof config;
// 'apiUrl' | 'timeout' | 'retries' | 'debug'

// typeof with as const for literal types
const STATUS = {
  ACTIVE: 'active',
  INACTIVE: 'inactive',
  PENDING: 'pending',
} as const;

type StatusType = typeof STATUS;
// { readonly ACTIVE: 'active'; readonly INACTIVE: 'inactive'; readonly PENDING: 'pending' }

type StatusValue = typeof STATUS[keyof typeof STATUS];
// 'active' | 'inactive' | 'pending'

// typeof with functions
function createUser(name: string, age: number) {
  return { id: Date.now(), name, age };
}
type CreateUserFn = typeof createUser;
// (name: string, age: number) => { id: number; name: string; age: number }

typeof in a type position extracts the compile-time type of a value. It is essential for deriving types from runtime values without manual duplication.

Indexed Access Types (Lookup Types)

interface ApiResponse {
  user: {
    id: number;
    name: string;
    address: {
      street: string;
      city: string;
      country: string;
    };
  };
  posts: {
    id: number;
    title: string;
    body: string;
  }[];
}

// Access nested types using bracket notation
type UserType = ApiResponse['user'];
// { id: number; name: string; address: { street: string; city: string; country: string } }

type AddressType = ApiResponse['user']['address'];
// { street: string; city: string; country: string }

type PostType = ApiResponse['posts'][number];
// { id: number; title: string; body: string }

// Union key access
type UserIdOrName = ApiResponse['user']['id' | 'name'];
// number | string

// Array element type
const roles = ['admin', 'user', 'guest'] as const;
type Role = typeof roles[number];
// 'admin' | 'user' | 'guest'

Indexed access types let you drill into nested types using bracket notation, just like accessing object properties at runtime. Use [number] to get the element type of an array.

Key points

Concepts covered

keyof, typeof, Indexed Access Types, Lookup Types, Type Queries