Skip to content
JL

Home

About

Blog

Contact

Shop

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. Indexed Access Types in TypeScript

Table of contents

  • Share on X
  • Discuss on X

Related Articles

Recursive Types in TypeScript
TypeScript
10m
Aug 9, 2026

Recursive Types in TypeScript

Some data structures are inherently self-referential — a tree node that contains other tree nodes, a JSON value that can itself be a JSON object containing more JSON values, a linked list whose nodes point to other nodes of the same type. TypeScript's recursive types handle all of these by allowing a type alias to reference itself in its own definition.

#Recursive Types#TypeScript Advanced Types+5
Template Literal Types in TypeScript
TypeScript
9m
Aug 2, 2026

Template Literal Types in TypeScript

Template literal types bring JavaScript's template literal syntax to the type level. They let you construct new string literal types by combining existing ones — the same way a template literal assembles a string at runtime, but resolved entirely at compile time.

#Template Literal Types#String Manipulation Types+5
Mapped Types: Transforming Types in TypeScript
TypeScript
10m
Jul 12, 2026

Mapped Types: Transforming Types in TypeScript

Mapped types are TypeScript's for-loop for types. They iterate over the keys of an existing type and produce a new type with transformed properties — the same way Array.map transforms every element in an array.

#Mapped Types#TypeScript Transformation+5
Ask me anything! 💬

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2026 • All rights reserved

Contents

  • What Are Indexed Access Types
  • Basic Property Access
  • Accessing Multiple Keys at Once
  • Array Element Types
  • Nested Type Access
  • Using Indexed Access with Generics
  • Real-World Patterns
  • Best Practices
  • What Is Next
  • Key Takeaways
TypeScript

Indexed Access Types in TypeScript

July 26, 2026•6 min read
Joshua R. Lehman
Joshua R. Lehman
Author
TypeScript indexed access types for extracting property and element types
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 | string

This 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"]; // number

Each [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 property K in type T — the type-level equivalent of runtime bracket notation
  • T[keyof T] produces a union of all value types in an object type
  • T[number] extracts the element type of an array or tuple
  • typeof 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.