Problem Solved

TypeScript · keyof

Home › Courses › TypeScript › Problems › keyof
🔷 Problem · 5 min read

How to use keyof.
Key unions and indexed access.

keyof is one of TypeScript's three type operators (along with typeof and indexed access T[K]). It turns an object type into a union of its keys. Combined with T[K] as a return type, it's the pattern behind every typed getter, pluck function, and property setter in the ecosystem. Learn it once, recognize it everywhere.

Short answer: keyof T gives a union of T's keys: keyof User = 'id' | 'name' | 'email'. T[K] gives the value type at key K: User['age'] = number. Together: function get(obj: T, key: K): T[K] — the typed getter that returns the exact type of the value at the given key. This is the pattern behind lodash's get, every typed accessor library, and React Hook Form's internals.

✅ Solution 1 — keyof produces a key union

The type-level equivalent of Object.keys.

When you write keyof User, TypeScript produces a union of every key of User. It's the compile-time version of Object.keys(user) — but it produces a union type, not a runtime array, and it lives in the type system. This union is the constraint that makes typed property access possible.

keyof.ts
// ─── keyof gives a union of keys ─────────────
interface User {
id: string;
name: string;
email: string;
age: number;
}

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

const k1: UserKey = 'name'; // ✅
const k2: UserKey = 'age'; // ✅
// const k3: UserKey = 'foo'; // ❌ 'foo' not a key of User

// ─── keyof typeof — keys from a value ────────
const config = {
host: 'localhost',
port: 3000,
debug: true
};

type ConfigKey = keyof typeof config;
// 'host' | 'port' | 'debug'

const ck: ConfigKey = 'port'; // ✅
// const bad: ConfigKey = 'url'; // ❌

// ─── keyof with generics ─────────────────────
function pluckKeys(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}

const keys = pluckKeys({ a: 1, b: 2 }); // ('a' | 'b')[]

// ─── keyof on unions ─────────────────────────
type A = { a: string; b: number };
type B = { b: number; c: boolean };
type AB = keyof (A | B);
// 'b' — only keys present in BOTH A and B
// (you can't safely access a key that's on only one member)

// ─── keyof on interfaces with index signatures
interface Dict {
[key: string]: number;
}
type DictKey = keyof Dict;
// string | number (the index signature's key type)

console.log(k1, k2, ck, keys);
// keyof is erased. Only the runtime values remain.

const k1 = 'name';
const k2 = 'age';

const config = {
host: 'localhost',
port: 3000,
debug: true
};

const ck = 'port';

function pluckKeys(obj) {
return Object.keys(obj);
}

const keys = pluckKeys({ a: 1, b: 2 });

console.log(k1, k2, ck, keys);

keyof T → key union

Produces a union of T's property names. keyof User = 'id' | 'name' | 'email' | 'age'. Compile-time only.

keyof typeof

Extract keys from a runtime value. typeof config is the type; keyof gives its keys. Standard pattern for config objects.

Works with generics

keyof T inside a generic function is the union of T's keys. This is what enables patterns.

On unions: intersection

keyof (A | B) is only the keys present in both. You can't safely access a key that exists on only one member.

✅ Solution 2 — T[K] indexed access

Get the type of the value at a key.

Indexed access T[K] is the type-level equivalent of obj[key]. It gives the exact type of the value at key K. Combined with keyof as a constraint, it produces typed getters that return different types based on which key was passed — the pattern that powers every typed property accessor.

indexed-access.ts
interface User {
id: string;
name: string;
age: number;
active: boolean;
}

// ─── Indexed access — the type of a value ────
type IdType = User['id']; // string
type AgeType = User['age']; // number
type ActiveType = User['active']; // boolean

// ─── Union indexed access ────────────────────
type AllValues = User[keyof User];
// string | number | boolean
// — the union of every value type in User.

// ─── The typed getter ────────────────────────
function getextends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const user: User = { id: '1', name: 'Alice', age: 28, active: true };

const n = get(user, 'name'); // string
const a = get(user, 'age'); // number
const b = get(user, 'active'); // boolean

// ─── The typed setter ────────────────────────
function setextends keyof T>(obj: T, key: K, value: T[K]): T {
return { ...obj, [key]: value };
}

const updated = set(user, 'name', 'Alicia'); // ✅ value: string
// set(user, 'age', 'x'); // ❌ value must be number
// set(user, 'name', 42); // ❌ value must be string

// ─── Multiple keys — union access ────────────
function pluckextends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}

const users: User[] = [user, { id: '2', name: 'Bob', age: 34, active: false }];

const names = pluck(users, 'name'); // string[]
const ages = pluck(users, 'age'); // number[]

// ─── Nested key access ───────────────────────
interface Order {
customer: { name: string; email: string };
total: number;
}

type CustomerType = Order['customer']; // { name, email }
type CustomerName = Order['customer']['name']; // string

console.log(n, a, b, updated, names, ages);
function get(obj, key) {
return obj[key];
}

const user = { id: '1', name: 'Alice', age: 28, active: true };

const n = get(user, 'name');
const a = get(user, 'age');
const b = get(user, 'active');

function set(obj, key, value) {
return { ...obj, [key]: value };
}

const updated = set(user, 'name', 'Alicia');

function pluck(items, key) {
return items.map(item => item[key]);
}

const users = [user, { id: '2', name: 'Bob', age: 34, active: false }];

const names = pluck(users, 'name');
const ages = pluck(users, 'age');

console.log(n, a, b, updated, names, ages);

T[K] gets the value type

User['age'] = number. It's the type of the value at key 'age'. Combine with keyof for the union of all values.

The typed getter

get(obj, key): T[K] returns the exact value type per key. get(user, 'age') is number, not the union.

Value must match key

The setter's value: T[K] enforces that 'name' requires a string and 'age' requires a number. Catches mismatches at compile time.

Nested access

Order['customer']['name'] works — indexed access composes. Drill into nested types without losing precision.

get(obj, key): T[K] is the most-used generic signature in TypeScript. Open any typed library: lodash's get, Immer's produce, React Hook Form's register, TanStack Table's accessors — every one uses this pattern or a variation. Master this one signature and you can read the source of every typed utility. It's also the correct signature for any custom getter you write — don't reinvent it, use it.

✅ Solution 3 — keyof typeof and real-world patterns

Extract keys from runtime objects, discriminate unions, and constrain APIs.

The most common real-world use: extracting keys from a runtime constant — a config object, a routes table, a feature flags list. keyof typeof value combines both operators: first extract the type with typeof, then extract its keys with keyof. It's the idiom for any keyed lookup table.

keyof-typeof.ts
// ─── Config objects ──────────────────────────
const config = {
apiUrl: 'https://api.example.com',
timeout: 3000,
retries: 5
} as const;

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

function getConfigextends ConfigKey>(key: K): typeof config[K] {
return config[key];
}

const url = getConfig('apiUrl'); // 'https://api.example.com'
const t = getConfig('timeout'); // 3000
// getConfig('port'); // ❌ 'port' not in config

// ─── Route definitions ───────────────────────
const routes = {
home: '/',
about: '/about',
blog: '/blog',
post: '/blog/:slug'
} as const;

type RouteName = keyof typeof routes;
// 'home' | 'about' | 'blog' | 'post'

function navigate(name: RouteName): void {
console.log('Navigate to:', routes[name]);
}

navigate('home'); // ✅
navigate('blog'); // ✅
// navigate('posts'); // ❌

// ─── Discriminated unions on objects ─────────
type EventMap = {
click: { x: number; y: number };
focus: { target: string };
blur: { target: string };
};

type EventName = keyof EventMap; // 'click' | 'focus' | 'blur'

function emitextends EventName>(event: E, payload: EventMap[E]): void {
console.log(event, payload);
}

emit('click', { x: 10, y: 20 }); // ✅ payload must have x, y
emit('focus', { target: 'input' }); // ✅ payload must have target
// emit('click', { target: 'x' }); // ❌ payload needs x, y
// emit('unknown', {}); // ❌ 'unknown' not an event

// ─── The pattern ─────────────────────────────
// The combination of:
// - EventMap (object type)
// - keyof EventMap (event names)
// - EventMap[E] (payload type per event)
//
// gives you a fully-typed event emitter with
// zero runtime overhead. This is how Zustand,
// Mitt, and every event bus types itself.

// ─── keyof + mapped types ────────────────────
type DeepReadonly = {
readonly [K in keyof T]: T[K]
};

type FrozenConfig = DeepReadonly<typeof config>;
// All config fields become readonly
// This is how utility types are built — keyof as the iteration

console.log(url, t);
navigate('home');
emit('click', { x: 1, y: 2 });
const config = {
apiUrl: 'https://api.example.com',
timeout: 3000,
retries: 5
};

function getConfig(key) {
return config[key];
}

const url = getConfig('apiUrl');
const t = getConfig('timeout');

const routes = {
home: '/',
about: '/about',
blog: '/blog',
post: '/blog/:slug'
};

function navigate(name) {
console.log('Navigate to:', routes[name]);
}

navigate('home');
navigate('blog');

function emit(event, payload) {
console.log(event, payload);
}

emit('click', { x: 10, y: 20 });
emit('focus', { target: 'input' });

console.log(url, t);
navigate('home');
emit('click', { x: 1, y: 2 });

Config schemas

keyof typeof config gives the config's keys. The typed getter returns exact value types — no casting, no any.

Routes and navigation

keyof typeof routes constrains navigation names. navigate('home') works; navigate('posts') is a compile error.

Typed event emitters

The EventMap + keyof + T[K] pattern gives an emitter where emit('click', {x, y}) is fully typed.

Foundation of mapped types

Every utility type (Partial, Readonly, Pick) uses keyof to iterate over keys. Mapped types are [K in keyof T].

⚠️ Common mistakes

The keyof bugs that cost real time.

1. Confusing keyof with Object.keys

keyof is type-only, produces a union. Object.keys is runtime, produces string[]. Fix: use keyof for types, Object.keys for iteration.

2. Forgetting extends keyof T in the signature

function get(obj: T, key: K) — K is unconstrained. get(user, 'foo') compiles. Fix: K extends keyof T.

3. Using K instead of T[K] as return type

function get(obj: T, key: K): K returns the key, not the value. Fix: : T[K].

4. Assuming keyof (A | B) includes all keys

On a union of objects, keyof gives only keys present in both. Fix: for a union's union, use keyof A | keyof B.

5. Using keyof on a value without typeof

keyof config fails — config is a value, not a type. Fix: keyof typeof config.

6. Expecting runtime key iteration

keyof doesn't produce a runtime array. You can't for...of on keyof T. Fix: use Object.keys at runtime, cast to (keyof T)[].

7. Losing keys with as const

Without as const, a literal object's values widen to their primitive types. Fix: as const preserves the exact literal types.

8. Using keyof with index signatures

keyof { [k: string]: number } = string | number — not literal keys. Fix: use a specific interface, not an index signature, when you need literal keys.

❓ Frequently asked

What does keyof do in TypeScript?

keyof T produces a union of T's property names. If User = { id: string; name: string; email: string }, then keyof User = 'id' | 'name' | 'email'. It's the type-level equivalent of Object.keys(), but it works on types, not values, and produces a compile-time union rather than a runtime array.

What is indexed access type (T[K])?

T[K] is a type operator that extracts the type of the value at key K. If User = { name: string; age: number }, then User['name'] = string and User['age'] = number. Combined with keyof as a constraint, T[K] gives the exact return type for a typed getter.

What is the difference between keyof and Object.keys?;

keyof is a type operator, exists only at compile time, and produces a union type ('a' | 'b'). Object.keys() is a runtime function, returns string[], and loses key information. Use keyof for type constraints and generic signatures. Use Object.keys() when iterating runtime object properties.

What is keyof typeof?

keyof typeof X combines both operators. typeof X extracts the type of a runtime value X, then keyof extracts the keys of that type. For a config object const config = { host: 'x', port: 3000 }, keyof typeof config = 'host' | 'port'. This is the standard way to extract keys from a runtime object.

🔗 Related

← All TypeScript problems
Problem solved · TypeScript