TypeScript Indexed Access Types
An indexed access type lets you look up the type of a property on another type, using the exact same square-bracket syntax you’d use to read a value at runtime — but entirely at the type level. Instead of retyping a property’s shape by hand, you write Product["price"] and TypeScript hands you back whatever type that property already has. This keeps types in sync automatically: change the source interface, and every type derived from it via indexed access updates too, with zero duplication.
Overview: How Indexed Access Types Work
TypeScript’s type system treats an object type as something you can index into, just like a value. If T is a type and K is a key (or a union of keys) that exists on T, then T[K] is a valid type expression that evaluates to the type of the property (or properties) named by K. This is often called a "lookup type."
The key K doesn’t have to be a single string literal. It can be a union of string literals (giving you a union of the corresponding value types), the special number type (useful for extracting the element type of an array or tuple), or the keyof T operator (giving you the union of every property’s type on T). Because keyof T produces a union of all the property names of T, combining it with indexed access — T[keyof T] — is an extremely common pattern for saying "any value that could appear on this type."
Indexed access types are resolved entirely by the compiler during type checking. They participate in TypeScript’s structural type system: two types built via different indexed access expressions are compatible if the resulting shapes match, regardless of how you arrived at them. And because all TypeScript types are erased when compiling to JavaScript, none of this appears in the emitted output — the compiled JS simply reads product.price like any ordinary property access, with no trace of the type-level lookup that validated it.
Indexed access types are especially powerful when combined with keyof, mapped types, and generics, because they let you write utilities that derive new types from a single source of truth instead of hand-maintaining parallel type declarations that can drift out of sync.
Syntax
Type["propertyName"] // type of a single named property
Type["propA" | "propB"] // union of a subset of property types
Type[keyof Type] // union of every property's type
Type[number] // element type of an array or tuple
Type["outer"]["inner"] // chained access into nested types
Type— any object type: aninterface, atypealias for an object shape, a class instance type, an array type, or a tuple type.- the bracketed key — a string literal type naming a property, a union of string literal types, the
keyof Typeoperator, ornumberfor indexed collections. - chaining — you can index into the result of an indexed access again, walking down through nested object types one level at a time.
- The key must actually exist on
Type(or be assignable to its index signature); referencing a property that isn’t there is a compile-time error, notundefinedat runtime.
Examples
Example 1: Extracting a single property’s type
interface Product {
id: number;
name: string;
price: number;
tags: string[];
}
type ProductName = Product["name"];
type ProductPrice = Product["price"];
const name: ProductName = "Wireless Mouse";
const price: ProductPrice = 29.99;
console.log(name, price);
Output:
Wireless Mouse 29.99
Here Product["name"] evaluates to string and Product["price"] evaluates to number, exactly matching the property declarations on Product. If price later changed to allow number | null, ProductPrice would automatically pick up that change — no separate type to update.
Example 2: Combining with keyof for a value union
interface Config {
host: string;
port: number;
secure: boolean;
}
type ConfigValue = Config[keyof Config];
function logConfigValue(value: ConfigValue): void {
console.log("Value:", value);
}
logConfigValue("localhost");
logConfigValue(8080);
logConfigValue(true);
Output:
Value: localhost
Value: 8080
Value: true
keyof Config is the union "host" | "port" | "secure". Indexing Config with that whole union produces the union of all three property types: string | number | boolean. This is exactly the pattern used internally by utility types like Record and by generic helper functions that need to accept "any value this object could hold."
Example 3: Array element types and nested access
interface Order {
id: number;
items: {
sku: string;
quantity: number;
}[];
customer: {
name: string;
address: {
city: string;
zip: string;
};
};
}
type OrderItem = Order["items"][number];
type CustomerCity = Order["customer"]["address"]["city"];
const item: OrderItem = { sku: "ABC-123", quantity: 2 };
const city: CustomerCity = "Springfield";
function describeItem(orderItem: OrderItem): string {
return `${orderItem.quantity}x ${orderItem.sku}`;
}
console.log(describeItem(item));
console.log(city);
Output:
2x ABC-123
Springfield
Order["items"] is an array type, so indexing it again with number yields the type of a single element — this is the standard idiom for pulling the element type out of an array or tuple without redeclaring the shape. CustomerCity shows that indexed access chains: each bracket step walks one level deeper into the nested object structure, ending at the innermost string property.
Under the Hood: Step by Step
When the compiler encounters T[K], it performs roughly these steps:
- It resolves
Tto its full structural shape (its set of known properties and their types, plus any index signatures). - It resolves
Kto a concrete key or set of keys — a string literal, a union of literals,number, or the result of expandingkeyof T. - For each resolved key, it looks up the corresponding property type on
T‘s shape. If a key doesn’t exist andThas no compatible index signature, this is a compile error. - If
Kwas a union, the compiler distributes the lookup over each member and combines the results into a union type. - The resulting type is substituted wherever
T[K]appeared, and ordinary type checking (assignability, inference, narrowing) proceeds using that substituted type.
All of this happens purely during type checking. Once the code compiles to JavaScript, every type annotation, interface, and indexed access expression is stripped away — the emitted JS is just plain property access (order.items[0], config.port, etc.). Indexed access types cost nothing at runtime; they exist solely to keep the compiler honest about what shapes flow through your program.
Common Mistakes
Mistake 1: Indexing a property that doesn’t exist
interface User {
id: number;
email: string;
}
type UserRole = User["role"];
This fails to compile with an error similar to Property 'role' does not exist on type 'User', because User was never declared with a role property. Indexed access types are not optional lookups — the key must be a real member of the type (or covered by an index signature).
Corrected version, once role is actually part of the interface:
interface User {
id: number;
email: string;
role: "admin" | "member";
}
type UserRole = User["role"];
const role: UserRole = "admin";
console.log(role);
Output:
admin
Mistake 2: Forgetting that optional properties include undefined
interface Settings {
theme: "light" | "dark";
timeout?: number;
}
type Timeout = Settings["timeout"];
function setTimeoutValue(value: Timeout): void {
console.log(value + 100);
}
Because timeout is optional, Settings["timeout"] is number | undefined, not just number. Under strict mode, value + 100 fails with an error like Object is possibly 'undefined', since arithmetic isn’t defined on undefined.
Corrected version, handling the undefined case explicitly:
interface Settings {
theme: "light" | "dark";
timeout?: number;
}
type Timeout = Settings["timeout"];
function setTimeoutValue(value: Timeout): void {
const resolved = value ?? 30;
console.log(resolved + 100);
}
setTimeoutValue(undefined);
setTimeoutValue(500);
Output:
130
600
Best Practices
- Prefer
Type["prop"]over manually copying a property’s type — it stays correct automatically as the source type evolves. - Use
Type[keyof Type]when you need "any value this object could hold," instead of writing out the union of value types by hand. - Use
ArrayOrTupleType[number]as the idiomatic way to extract an element type from an array or tuple. - Remember optional properties (
prop?: X) surface asX | undefinedthrough indexed access — handle that union explicitly rather than asserting it away. - Chain indexed access (
Type["a"]["b"]) to drill into nested shapes instead of re-declaring inner interfaces separately. - Combine indexed access with generics (for example
function get<T, K extends keyof T>(obj: T, key: K): T[K]) when you need a reusable, type-safe property accessor.
Practice Exercises
- Define an interface
Invoicewithid: number,amount: number, andstatus: "draft" | "sent" | "paid". Use indexed access to declare a typeInvoiceStatusequal to the type of thestatusproperty, then write a function that accepts only that type and logs it. - Given
interface Catalog { books: { title: string; year: number }[]; }, use indexed access (twice, chained) to derive a type representing a single book, and write a function that formats a book as"Title (Year)". - Given an interface with several properties of mixed types, build a type using
Type[keyof Type]representing the union of all its value types, then write a function that accepts that union and uses atypeofcheck to branch on the runtime type before logging it.
Summary
- Indexed access types use the syntax
Type["key"]to read off the type of a property from an existing type, keeping derived types automatically in sync with their source. - The key can be a single string literal, a union of literals,
keyof Type(all property types as a union), ornumber(element type of an array or tuple). - Indexed access chains:
Type["a"]["b"]walks into nested object shapes one level at a time. - Optional properties surface as
T | undefinedthrough indexed access — handle that explicitly understrictmode. - All of this is compile-time only; indexed access types are fully erased and have zero runtime cost.
