Difficulty: Intermediate
Explain the keyof and typeof operators in TypeScript. How do they work together for type-safe property access?
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.
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 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.
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.
keyof, typeof, Indexed Access Types, Lookup Types, Type Queries