Question

Extract Interface Property Types in TypeScript with Indexed Access Types

typescripttypingdefinitelytyped

Question

Suppose a TypeScript declaration file for library X contains these interfaces:

interface I1 {
  x: any;
}

interface I2 {
  y: {
    a: I1;
    b: I1;
    c: I1;
  };
  z: any;
}

When working with this library, I need to pass around an object with the same type as the y property of I2. I could duplicate that shape in my own interface:

interface MyInterface {
  a: I1;
  b: I1;
  c: I1;
}

let myVar: MyInterface;

However, this duplicates library types and requires maintenance if the library changes. Is there a way to extract the type of a specific interface property directly, similar to typeof I2.y? A solution using a declared value works, but I want to do this without declaring a redundant variable:

declare const value: I2;
let y: typeof value.y;

Short Answer

TypeScript can extract a property type directly from an interface with an indexed access type. Write I2["y"] to reuse the type of I2's y property without creating a duplicate interface or a placeholder variable. This keeps your code synchronized when the library updates its type definitions.

Concept

An interface is a type, not a runtime JavaScript value. That is why this does not work:

// Invalid: I2 is a type, not an object available at runtime.
type Y = typeof I2.y;

The typeof operator in a type position gets the type of a value that exists (or is declared) as a value. For example, if value is a variable, typeof value produces its type.

To look up a member of a type directly, TypeScript provides indexed access types. They use square brackets:

type Y = I2["y"];

I2["y"] means: “get the type associated with the y key in I2.” It is the type-level equivalent of reading object["y"] at runtime.

This matters because library declarations often contain nested object shapes, callback signatures, configuration objects, and API response types. Reusing a property type avoids copying it into your project, so changes made by the library are automatically reflected in your code when you update its typings.

Mental Model

Think of an interface as a blueprint with labeled rooms.

  • I2 is the complete blueprint.
  • y is one room inside that blueprint.
  • I2["y"] asks TypeScript to give you the blueprint for that one room.

You do not need to build a fake house (declare const value: I2) just to inspect a room. Indexed access lets you refer to the room directly from the blueprint.

Take Quiz

Syntax and Examples

Use this syntax to extract a property type:

type PropertyType = InterfaceName["propertyName"];

For the interfaces in the question:

interface I1 {
  x: any;
}

interface I2 {
  y: {
    a: I1;
    b: I1;
    c: I1;
  };
  z: any;
}

type I2Y = I2["y"];

const value: I2Y = {
  a: { x: "first" },
  b: { x: 2 },
  c: { x: true }
};

I2Y is equivalent to the object type declared for I2.y:

{
  a: I1;
  b: I1;
  c: I1;
}

You can also extract several property types at once by using a union of keys:

type YOrZ = I2["y" | "z"];
// { a: I1; b: I1; c: I1 } | any

In practice, avoid a union containing any, because any makes the resulting type effectively unrestricted.

Step by Step Execution

Consider this code:

interface User {
  id: string;
  profile: {
    displayName: string;
    email: string;
  };
}

type Profile = User["profile"];

const profile: Profile = {
  displayName: "Ada",
  email: "ada@example.com"
};

Step by step:

  1. User defines an object type with id and profile properties.

  2. User["profile"] performs a type lookup using the key "profile".

  3. TypeScript finds the declared type of profile inside User.

  4. Profile becomes:

    {
      displayName: string;
      email: string;
    }
    
  5. The profile variable must contain both required fields as strings.

  6. This would cause a type error because email is missing:

    const invalidProfile: Profile = {
      displayName: "Ada"
    };
    

Real World Use Cases

Indexed access types are useful whenever one type is the source of truth and you need one part of it elsewhere.

  • Library configuration: Reuse one section of a third-party library's options type.

    type DatabaseOptions = AppOptions["database"];
    
  • API data: Extract the type of a nested API response field.

    type UserRecord = ApiResponse["user"];
    
  • Event handlers: Reuse a callback type from an interface.

    type OnSave = EditorOptions["onSave"];
    
  • Component props: Share the type of a nested prop between UI components.

    type ButtonStyle = Theme["button"];
    
  • State models: Extract one slice of application state.

    type SessionState = RootState["session"];
    
Take Quiz

Real Codebase Usage

In production code, give extracted types meaningful aliases rather than repeatedly writing long lookups:

type RequestHeaders = HttpRequest["headers"];
type RequestBody = HttpRequest["body"];

This makes function signatures easier to read:

function validateHeaders(headers: RequestHeaders): boolean {
  return headers.authorization !== undefined;
}

Indexed access is also commonly combined with generics. A reusable helper can return the value type for any valid object key:

type ValueOf<T, K extends keyof T> = T[K];

interface Settings {
  retryCount: number;
  endpoint: string;
}

type Endpoint = ValueOf<Settings, "endpoint">;
// string

A frequent pattern is to derive an element type from an array property:

interface OrderResponse {
  orders: Array<{ id: string; total: number }>;
}

type Order = OrderResponse["orders"][number];
// { id: string; total: number }

This is especially useful for API clients: define the response once, then derive types for list items, function parameters, validation functions, and UI rendering code.

Common Mistakes

Using typeof with an interface name

This is invalid:

interface I2 {
  y: string;
}

type Y = typeof I2.y;

I2 is erased when TypeScript becomes JavaScript. It is not a runtime object, so typeof has no value to inspect. Use I2["y"] instead.

Using dot notation in a type lookup

This is not TypeScript's indexed-access syntax:

// Invalid
type Y = I2.y;

Use square brackets and a key:

type Y = I2["y"];

Misspelling a property key

TypeScript rejects keys that do not exist:

// Error: Property 'missing' does not exist on type 'I2'.
type Missing = I2["missing"];

This is helpful: it catches stale property names when a library changes.

Duplicating a library type unnecessarily

Avoid manually recreating I2["y"] unless you intentionally want a different, independent type. A copied interface can silently become outdated after a dependency upgrade.

Confusing type compatibility with exact object shapes

TypeScript uses structural typing. A value can have extra properties and still be assignable when it comes from a variable:

type Y = I2["y"];

const source = {
  a: { x: 1 },
  b: { x: 2 },
  c: { x: 3 },
  extra: true
};

const y: Y = source; // Allowed

I2["y"] gives the same declared property type; it does not generally enforce that an object has only those properties.

Comparisons

Tool or syntaxWhat it doesWhen to use it
I2["y"]Extracts the type of property y from type I2You have a type/interface and need one of its property types
typeof valueGets the type of a runtime value or declared variableYou already have a value, function, class, or constant
keyof I2Produces a union of keys, such as "y" | "z"You need valid property names
I2[keyof I2]Produces a union of all property value typesYou need any value type from an object type
I2["y"]["a"]Extracts a nested property typeYou need a deeper part of a type

Here is a useful combination of keyof and indexed access:

function getValue<T, K extends keyof T>(object: T, key: K): T[K] {
  return object[key];
}

K extends keyof T ensures that key is valid, while T[K] represents the type stored at that key.

Take Quiz

Cheat Sheet

// Extract one property type
type Y = I2["y"];

// Extract a nested property type
type A = I2["y"]["a"];

// Extract types for multiple keys
type Values = I2["y" | "z"];

// Get every key
type Keys = keyof I2;

// Get a union of every property value type
type AllValues = I2[keyof I2];

// Generic property lookup
type ValueOf<T, K extends keyof T> = T[K];

// Get an array element type
type Item = Response["items"][number];

Rules to remember:

  • Use Type["key"], not Type.key.
  • The key must exist on the type.
  • typeof is for values; indexed access is for properties of types.
  • Types are compile-time only and do not create JavaScript code.
  • Type["key"] reuses the declared type but does not guarantee an object has no additional properties.

FAQ

How do I get the type of an interface property in TypeScript?

Use an indexed access type:

type PropertyType = MyInterface["propertyName"];

For the question's example, use type I2Y = I2["y"];.

Why does typeof I2.y not work?

I2 is an interface, which exists only in TypeScript's type system. It is not a runtime JavaScript object. typeof in a type annotation needs a value to inspect.

Can I extract a nested property type?

Yes. Chain indexed accesses:

type AType = I2["y"]["a"];

Do I need to declare a variable first?

No. declare const value: I2; type Y = typeof value.y; works, but type Y = I2["y"]; is direct and clearer.

Can I use a property name stored in a type alias?

Yes, as long as it is a valid key:

type Key = "y";
type Y = I2[Key];

What happens if the library changes the type of I2.y?

Your alias automatically reflects the new declaration after you update the library's type definitions. This is the main advantage over copying the object shape.

Does I2["y"] enforce an exact object shape?

No. TypeScript is structurally typed, so values with additional properties can often still be assigned. It enforces that required properties and their types are compatible.

Take Quiz

Mini Project

Description

Build a small type-safe formatter for a library-style API response. You will define the response once and derive the nested user and item types from it, rather than duplicating those shapes in function signatures.

Goal

Create functions that accept extracted nested types and format a user summary and order total.

Requirements

Define an ApiResponse interface with user and orders properties. Derive a User type from the user property. Derive an Order type from an element of the orders array. Write a function that formats a user summary. Write a function that calculates the total from an array of orders.

Take Quiz

Keep learning

Related questions

View more