Indexed Access Types in TypeScript


Indexed access types let you look up the type of a property using the same bracket notation you use at runtime. Just as user["name"] gives you the value of the name property, User["name"] gives you the type of that property. This sounds simple, but it's one of the most practical tools in advanced TypeScript — it lets you derive types from parts of other types without duplicating definitions. In this post, you'll learn how to access property types, array element types, and deeply nested types using indexed access syntax.
Types, Not Values
T[K] in a type position is an indexed access type — it produces a type.
obj[key] in a value position accesses a value at runtime. They use identical
syntax but operate at completely different levels. Context determines which
you're writing.
What Are Indexed Access Types
The syntax T[K] in type position looks up the type of property K in type T:
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user" | "guest";
}
type UserName = User["name"]; // string
type UserId = User["id"]; // number
type UserRole = User["role"]; // "admin" | "user" | "guest"The key K must be a valid key of T — TypeScript will error if you try to access a property that doesn't exist:
type Bad = User["missing"];
// Error: Property 'missing' does not exist on type 'User'This is intentional — indexed access types are type-safe. They catch property name typos at the type level, the same way keyof prevents invalid property access at runtime.
Basic Property Access
You can use string literals directly or a type variable as the index:
// Direct string literal
type EmailType = User["email"]; // string
// Using keyof
type AnyUserProperty = User[keyof User];
// number | string | "admin" | "user" | "guest"
// (union of all property value types)T[keyof T] is a useful pattern — it produces a union of all value types in an object type.
Accessing Multiple Keys at Once
Pass a union type as the index to get a union of the corresponding value types:
type UserContactInfo = User["name" | "email"];
// string | string = string
type UserIdentifiers = User["id" | "email"];
// number | stringThis is equivalent to using Pick<User, "id" | "email"> and then taking keyof — but much more concise when you just need the value types.
Union Indices
T["a" | "b"] distributes just like conditional types — it resolves to
T["a"] | T["b"]. This makes it easy to extract value type unions for a
subset of keys.
Array Element Types
For array types, use number as the index to extract the element type:
type StringArray = string[];
type Element = StringArray[number]; // string
type MixedArray = (string | number | boolean)[];
type MixedElement = MixedArray[number]; // string | number | boolean
// Works with tuple types too
type Pair = [string, number];
type First = Pair[0]; // string
type Second = Pair[1]; // number
type Either = Pair[number]; // string | number (union of all element types)The [number] index works because arrays are typed as { [n: number]: T } internally — numeric indices are valid keys.
// Very useful for extracting types from literal arrays
const ROLES = ["admin", "user", "guest"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "user" | "guest"This typeof ROLES[number] pattern is idiomatic TypeScript for deriving a union type from a constant array — no separate type declaration needed.
The Best Enum Alternative
const ROLES = [...] as const plus type Role = typeof ROLES[number] is one
of the most popular patterns in modern TypeScript. You get a runtime array you
can iterate, and a type-safe union from the same definition.
Nested Type Access
You can chain indexed access to drill into nested types:
interface Order {
id: string;
customer: {
id: number;
name: string;
address: {
street: string;
city: string;
country: string;
};
};
items: Array<{
productId: string;
quantity: number;
price: number;
}>;
}
type CustomerType = Order["customer"]; // { id: number; name: string; address: {...} }
type AddressType = Order["customer"]["address"]; // { street: string; city: string; country: string }
type CityType = Order["customer"]["address"]["city"]; // string
type ItemType = Order["items"][number]; // { productId: string; quantity: number; price: number }
type ItemPrice = Order["items"][number]["price"]; // numberEach [Key] step drills one level deeper. This is far better than duplicating the nested type definition — the chain stays in sync when the source type changes.
Using Indexed Access with Generics
Indexed access types become especially powerful in generic contexts, combined with keyof:
// Type-safe getter — seen before, now making sense in depth
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Type-safe deep getter for two levels
function getDeep<T, K1 extends keyof T, K2 extends keyof T[K1]>(
obj: T,
key1: K1,
key2: K2
): T[K1][K2] {
return obj[key1][key2];
}
const order: Order = {
id: "ord-1",
customer: {
id: 42,
name: "Alice",
address: { street: "1 Main", city: "Toronto", country: "CA" },
},
items: [],
};
const city = getDeep(order, "customer", "address");
// ^? { street: string; city: string; country: string }Deep Generics Get Verbose
The two-level deep getter requires three type parameters. For deeper nesting,
consider a different approach — recursive types or a library like
ts-toolbelt that provides deep path access. The built-in approach doesn't
scale past 2-3 levels.
Real-World Patterns
A common real-world use: deriving component prop types from a central data type without re-declaring them:
interface AppState {
user: {
id: number;
name: string;
preferences: {
theme: "light" | "dark";
language: string;
};
};
posts: Array<{
id: string;
title: string;
publishedAt: Date | null;
}>;
}
// Derive prop types from the central state
type UserCardProps = {
user: AppState["user"];
};
type PostListProps = {
posts: AppState["posts"];
onSelect: (post: AppState["posts"][number]) => void;
};
type ThemeToggleProps = {
theme: AppState["user"]["preferences"]["theme"];
onChange: (theme: AppState["user"]["preferences"]["theme"]) => void;
};When you update AppState, all derived prop types update automatically. No manual synchronization.
Best Practices
Use indexed access instead of duplicating type definitions. If a type already contains the shape you need, reach into it rather than declaring a new type that mirrors it.
Combine with typeof for runtime objects. The typeof obj["property"] pattern derives a type from a nested runtime value — useful for config objects and constants.
Use T[number] to extract array element types. It's more concise than T extends Array<infer E> ? E : never for most cases.
Prefer named extraction for complex types. Long chains like T["a"]["b"]["c"]["d"] are hard to maintain. Name intermediate steps:
type Customer = Order["customer"];
type Address = Customer["address"];
type Country = Address["country"];Anti-Pattern
Don't copy a nested interface's shape into a new standalone type. If
User["address"] gives you what you need, use it. A copied type will silently
diverge when the source changes — an indexed access type cannot.
What Is Next
You've now mastered the core type query operators. The next post explores template literal types — TypeScript's ability to manipulate string types at the type level. Combined with keyof and mapped types, they enable patterns like type-safe event names, CSS property strings, and API route validation.
Key Takeaways
T[K]looks up the type of propertyKin typeT— the type-level equivalent of runtime bracket notationT[keyof T]produces a union of all value types in an object typeT[number]extracts the element type of an array or tupletypeof ROLES[number]is the idiomatic pattern for deriving a union type from a const array- Chaining indexed access (
T["a"]["b"]) drills into nested types without duplicating definitions
Indexed access types keep your types DRY. Every time you reach into an existing type instead of redeclaring its shape, you create a binding that can never silently diverge — TypeScript will break loudly if the source type changes and you forget to update.