Indexed Access Types
Indexed access types — written T[K] — let you look up the type of a property by its key at the type level. Just as obj['name'] retrieves a value at runtime, T['name'] retrieves a type at compile time. This makes it possible to build types that are automatically derived from their parent, staying in sync as the parent changes.
Basic Syntax
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
address: {
street: string;
city: string;
country: string;
};
}
// Look up a single property type
type UserId = User['id']; // number
type UserName = User['name']; // string
type UserRole = User['role']; // 'admin' | 'user'
// Look up a nested property type
type UserCity = User['address']['city']; // string
type UserAddr = User['address']; // { street: string; city: string; country: string }T[K] must be a valid key of T — TypeScript will error if K is not assignable to keyof T.Union of Keys — Getting a Union of Value Types
When you index with a union of keys, TypeScript returns a union of the corresponding value types. Indexing with keyof T gives you the union of all value types.
interface Config {
host: string;
port: number;
debug: boolean;
timeout: number;
}
// Union of key types
type StringOrNumberConfig = Config['host' | 'port'];
// string | number
// All value types
type AnyConfigValue = Config[keyof Config];
// string | number | boolean
// Practical: get the type of a specific subset of properties
type NetworkConfig = Config['host' | 'port' | 'timeout'];
// string | numberIndexed Access on Arrays and Tuples
You can use indexed access on array types. Using number as the key extracts the element type of an array. For tuples, you can use specific numeric indices.
// Array element type type StringArray = string[]; type Element = StringArray[number]; // string // Useful for deriving types from const arrays const PERMISSIONS = ['read', 'write', 'admin', 'superuser'] as const; type Permission = typeof PERMISSIONS[number]; // 'read' | 'write' | 'admin' | 'superuser' // Tuple indexed access type Coordinate = [x: number, y: number, z: number]; type XType = Coordinate[0]; // number type YType = Coordinate[1]; // number type ZType = Coordinate[2]; // number type AnyCoord = Coordinate[number]; // number (union of all element types) // More complex tuple type HttpRequest = [method: string, url: string, body: unknown, headers: Record<string, string>]; type RequestMethod = HttpRequest[0]; // string type RequestBody = HttpRequest[2]; // unknown
typeof ARRAY[number] is the idiomatic way to derive a string union type from a const array. It is concise and automatically stays in sync if you add items to the array.Indexed Access in Generic Functions
// Safe property getter — T[K] is the exact return type
function getProperty<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 = getProperty(user, 'name'); // string (not any, not unknown)
const id = getProperty(user, 'id'); // number
// Property setter
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
return { ...obj, [key]: value };
}
const updated = setProperty(user, 'name', 'Bob'); // User — safe
// setProperty(user, 'name', 42); // ❌ Type 'number' is not assignable to 'string'
// Pluck multiple properties
function pluck<T, K extends keyof T>(arr: T[], key: K): T[K][] {
return arr.map(item => item[key]);
}
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
const ids = pluck(users, 'id'); // number[]
const names = pluck(users, 'name'); // string[]Deriving Types from Nested Structures
interface ApiSchema {
endpoints: {
getUser: { params: { id: number }; response: { id: number; name: string } };
listUsers: { params: { page: number }; response: { items: { id: number; name: string }[]; total: number } };
createUser: { params: { name: string; email: string }; response: { id: number } };
};
}
// Extract types for a specific endpoint
type GetUserParams = ApiSchema['endpoints']['getUser']['params']; // { id: number }
type GetUserResponse = ApiSchema['endpoints']['getUser']['response']; // { id: number; name: string }
type ListUsersResponse = ApiSchema['endpoints']['listUsers']['response'];
// { items: { id: number; name: string }[]; total: number }
// Generic helper to extract params and response for any endpoint
type EndpointParams<K extends keyof ApiSchema['endpoints']> =
ApiSchema['endpoints'][K]['params'];
type EndpointResponse<K extends keyof ApiSchema['endpoints']> =
ApiSchema['endpoints'][K]['response'];
async function callApi<K extends keyof ApiSchema['endpoints']>(
endpoint: K,
params: EndpointParams<K>
): Promise<EndpointResponse<K>> {
// implementation
return null as unknown as EndpointResponse<K>;
}
// Fully typed call — params and response are inferred
const response = await callApi('getUser', { id: 1 });
// response.name — stringCombining keyof and Indexed Access
// Build a type from another type's values
interface EventPayloads {
click: { x: number; y: number };
keydown: { key: string; code: string };
focus: { target: string };
blur: { target: string };
}
type AnyEventPayload = EventPayloads[keyof EventPayloads];
// { x: number; y: number } | { key: string; code: string } | { target: string }
// Extract the union of all response types from an API schema
interface Routes {
'/users': { GET: User[]; POST: User };
'/users/:id': { GET: User; PUT: User; DELETE: void };
'/posts': { GET: Post[]; POST: Post };
}
type AllResponses = Routes[keyof Routes][keyof Routes[keyof Routes]];
// User[] | User | void | Post[] | Post
interface User { id: number; name: string }
interface Post { id: number; title: string }Indexed Access with Mapped Types
// Filter an object type to only properties of a given value type
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
interface Profile {
id: number;
name: string;
email: string;
age: number;
verified: boolean;
score: number;
}
type StringFields = PickByValue<Profile, string>; // { name: string; email: string }
type NumberFields = PickByValue<Profile, number>; // { id: number; age: number; score: number }
type BooleanFields = PickByValue<Profile, boolean>; // { verified: boolean }
// Deep partial using indexed access recursively
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
type PartialUser = DeepPartial<User>;
// { id?: number; name?: string }Real-World Example: Type-Safe Form
// A generic form that tracks values, errors, and touched state for any shape
interface FormField<V> {
value: V;
error: string | null;
touched: boolean;
}
type FormState<T> = {
[K in keyof T]: FormField<T[K]>;
};
// Derive field value type from the form state
type FieldValue<TForm, K extends keyof TForm> =
TForm[K] extends FormField<infer V> ? V : never;
interface RegistrationForm {
username: string;
email: string;
password: string;
age: number;
}
type RegistrationFormState = FormState<RegistrationForm>;
// {
// username: FormField<string>;
// email: FormField<string>;
// password: FormField<string>;
// age: FormField<number>;
// }
type UsernameValue = FieldValue<RegistrationFormState, 'username'>; // string
type AgeValue = FieldValue<RegistrationFormState, 'age'>; // number
function getFieldValue<
TForm extends Record<string, FormField<unknown>>,
K extends keyof TForm
>(form: TForm, field: K): FieldValue<TForm, K> {
return form[field].value as FieldValue<TForm, K>;
}Practical: Strict Object Diff
// Return an object containing only the keys where old and new values differ
type ChangedFields<T> = Partial<{ [K in keyof T]: { from: T[K]; to: T[K] } }>;
function diff<T extends object>(before: T, after: T): ChangedFields<T> {
const changes: ChangedFields<T> = {};
const keys = Object.keys(before) as (keyof T)[];
for (const key of keys) {
if (before[key] !== after[key]) {
(changes[key] as { from: T[keyof T]; to: T[keyof T] }) = {
from: before[key],
to: after[key],
};
}
}
return changes;
}
const userBefore = { id: 1, name: 'Alice', email: 'alice@example.com' };
const userAfter = { id: 1, name: 'Alice Smith', email: 'alice@example.com' };
const changes = diff(userBefore, userAfter);
// { name: { from: 'Alice', to: 'Alice Smith' } }
console.log(changes.name?.from); // 'Alice'Indexed Access in Utility Type Implementation
Many of TypeScript's built-in utility types use indexed access under the hood. Understanding this helps you both read their definitions and write your own.
// ReturnType — extracts the return type of a function type
type MyReturnType<T extends (...args: unknown[]) => unknown> =
T extends (...args: unknown[]) => infer R ? R : never;
// Parameters — extracts parameter types as a tuple
type MyParameters<T extends (...args: unknown[]) => unknown> =
T extends (...args: infer P) => unknown ? P : never;
// PromiseType — unwraps a Promise
type PromiseType<T extends Promise<unknown>> =
T extends Promise<infer U> ? U : never;
// ArrayElement — element type of an array
type ArrayElement<T extends unknown[]> = T[number];
// Head of a tuple
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
// Tail of a tuple
type Tail<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : never;
// Usage
function fetchUsers(): Promise<{ id: number; name: string }[]> {
return Promise.resolve([]);
}
type UsersPromise = ReturnType<typeof fetchUsers>; // Promise<...[]>
type UserArray = PromiseType<UsersPromise>; // { id: number; name: string }[]
type SingleUser = ArrayElement<UserArray>; // { id: number; name: string }Advanced: Recursive Indexed Access
// Access deeply nested types without specifying the full path manually
type DeepGet<T, K extends string> =
K extends `${infer Head}.${infer Tail}`
? Head extends keyof T
? DeepGet<T[Head], Tail>
: never
: K extends keyof T
? T[K]
: never;
interface NestedConfig {
database: {
connection: {
host: string;
port: number;
};
name: string;
};
server: {
port: number;
};
}
type DBHost = DeepGet<NestedConfig, 'database.connection.host'>; // string
type DBPort = DeepGet<NestedConfig, 'database.connection.port'>; // number
type DBName = DeepGet<NestedConfig, 'database.name'>; // string
type SrvPort = DeepGet<NestedConfig, 'server.port'>; // numberDeepGet type uses template literal types to split dot-separated paths. This pattern appears in libraries like Lodash's type declarations and form libraries like React Hook Form.Summary of Patterns
Pattern | Syntax | Result |
|---|---|---|
Single property type | T['prop'] | Type of that property |
Union of properties | T['a' | 'b'] | Union of those property types |
All value types | T[keyof T] | Union of all value types |
Array element type | T[number] | Element type of array T |
Const array union | typeof arr[number] | Union of literal values |
Nested property | T['a']['b'] | Type of nested property |
Generic safe getter | T[K extends keyof T] | Exact type of property K |
Quick Reference
T['prop'] — look up the type of a property by name
T[K] where K extends keyof T — generic indexed access
T[keyof T] — union of all value types
typeof arr[number] — element type of a const array (use for string unions)
Nested access: T[K1][K2] — chain indexed access operators
Combine with mapped types: [K in keyof T]: SomeType<T[K]>
DeepGet pattern uses template literal types for dot-path access
keyof and typeof. Together, they are the building blocks of TypeScript's most powerful type-level programming patterns, including mapped types, conditional types, and the entire standard utility type library.