Skip to content
Codeloom
TypeScript

TypeScript typeof and keyof Operators in Practice

Master the typeof and keyof type operators to derive types from values and extract object keys for type-safe lookups and mappings.

·6 min read · By Codeloom
Intermediate 8 min read

What you'll learn

  • How typeof extracts a type from a runtime value
  • How keyof produces a union of an object type's keys
  • Combining typeof and keyof for derived types
  • Practical patterns for config objects, lookup tables, and event maps

Prerequisites

None — this post is self-contained.

TypeScript includes two type-level operators that derive new types from existing code. The typeof operator extracts a type from a runtime value, letting you avoid writing a type that duplicates a value you already have. The keyof operator produces a union of an object type’s property names. Together, they form the foundation for many advanced TypeScript patterns.

typeof in Type Position

JavaScript has a typeof operator that returns a string like "string" or "number" at runtime. TypeScript adds a second typeof that works at the type level. When typeof appears where a type is expected, it extracts the TypeScript type of a 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;
// }

Instead of writing a Config interface by hand and keeping it in sync with the value, typeof derives the type directly. If you add a property to config, the type updates automatically.

typeof With Functions

typeof is especially useful for extracting function types:

function createUser(name: string, age: number) {
  return { id: crypto.randomUUID(), name, age, createdAt: new Date() };
}

type CreateUserFn = typeof createUser;
// (name: string, age: number) => { id: string; name: string; age: number; createdAt: Date }

type User = ReturnType<typeof createUser>;
// { id: string; name: string; age: number; createdAt: Date }

The combination of ReturnType and typeof extracts the return type of a function without needing a separate interface. This is invaluable when working with factory functions or API handlers where the return shape is defined by implementation.

typeof With const Assertions

Pairing typeof with as const preserves literal types:

const ROLES = ['admin', 'editor', 'viewer'] as const;

type Role = (typeof ROLES)[number];
// 'admin' | 'editor' | 'viewer'

Without as const, typeof ROLES would be string[], and indexing it would give string. The const assertion preserves the literal values, and [number] indexes into the tuple to produce the union.

const STATUS_CODES = {
  OK: 200,
  NOT_FOUND: 404,
  SERVER_ERROR: 500,
} as const;

type StatusCode = (typeof STATUS_CODES)[keyof typeof STATUS_CODES];
// 200 | 404 | 500

keyof Basics

The keyof operator takes an object type and produces a union of its keys:

interface User {
  id: string;
  name: string;
  email: string;
  age: number;
}

type UserKey = keyof User;
// 'id' | 'name' | 'email' | 'age'

This union type restricts values to only valid property names, enabling type-safe property access.

Type-Safe Property Access

The most common use of keyof is constraining a function parameter to valid property names:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = {
  id: '1',
  name: 'Alice',
  email: 'alice@example.com',
  age: 30,
};

const name = getProperty(user, 'name');   // string
const age = getProperty(user, 'age');     // number
const bad = getProperty(user, 'address'); // Error: 'address' not in keyof User

The return type T[K] is an indexed access type that resolves to the specific property type. Passing 'name' returns string, passing 'age' returns number.

Combining typeof and keyof

The real power emerges when combining both operators. Given a value, extract its keys as a type:

const eventHandlers = {
  click: (x: number, y: number) => { /* ... */ },
  keydown: (key: string) => { /* ... */ },
  scroll: (offset: number) => { /* ... */ },
};

type EventName = keyof typeof eventHandlers;
// 'click' | 'keydown' | 'scroll'

function emit<K extends keyof typeof eventHandlers>(
  event: K,
  ...args: Parameters<(typeof eventHandlers)[K]>
): void {
  eventHandlers[event](...args);
}

emit('click', 100, 200);    // valid
emit('keydown', 'Enter');    // valid
emit('scroll', 50);          // valid
emit('click', 'wrong');      // Error: string not assignable to number
emit('hover', 0, 0);         // Error: 'hover' not in EventName

Each event name is constrained to existing keys, and the arguments are type-checked against the corresponding handler’s parameter types.

Building Lookup Tables

A pattern for configuration-driven code uses keyof to ensure exhaustive handling:

const validators = {
  string: (v: unknown): v is string => typeof v === 'string',
  number: (v: unknown): v is number => typeof v === 'number',
  boolean: (v: unknown): v is boolean => typeof v === 'boolean',
  array: (v: unknown): v is unknown[] => Array.isArray(v),
} as const;

type ValidatorType = keyof typeof validators;

function validate(value: unknown, type: ValidatorType): boolean {
  return validators[type](value);
}

validate('hello', 'string');  // valid
validate(42, 'number');       // valid
validate([], 'object');       // Error: 'object' not in ValidatorType

keyof With Mapped Types

keyof is a building block for mapped types that transform object shapes:

type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type NullableUser = Nullable<User>;
// {
//   id: string | null;
//   name: string | null;
//   email: string | null;
//   age: number | null;
// }

Filtering Keys by Value Type

Use keyof with conditional types to extract keys whose values match a certain type:

type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never;
}[keyof T];

type StringKeys = KeysOfType<User, string>;
// 'id' | 'name' | 'email'

type NumberKeys = KeysOfType<User, number>;
// 'age'

function getStringField(user: User, key: KeysOfType<User, string>): string {
  return user[key];
}

getStringField(user, 'name');  // valid, returns string
getStringField(user, 'age');   // Error: 'age' is not a string key

keyof With Index Signatures

When an object type has an index signature, keyof includes the index type:

interface StringMap {
  [key: string]: unknown;
  length: number;
}

type StringMapKeys = keyof StringMap;
// string | number (because numeric keys are a subtype of string keys)

Be aware of this when working with dictionary-like types. Named properties are included alongside the index signature.

Wrap Up

The typeof operator bridges values and types, letting you derive a type from any expression without manual duplication. The keyof operator extracts property names as a union, enabling type-safe access patterns. Combined, they let you write code where types flow from your data, configuration objects become the source of truth, and the compiler catches invalid property access at build time rather than in production.