TypeScript’s index signatures and mapped types can both create objects with dynamic properties, but they operate at fundamentally different levels of abstraction and serve distinct purposes.

Let’s see them in action.

Index Signatures

Index signatures are a way to describe the "shape" of an object when you don’t know all the property names in advance, but you know their types.

interface StringMap {
  [key: string]: string; // This is an index signature
}

const myMap: StringMap = {
  apple: "a fruit",
  banana: "a yellow fruit",
  // 'grape': 123 // Error: Type 'number' is not assignable to type 'string'.
};

console.log(myMap.apple);
console.log(myMap["banana"]);

Here, [key: string]: string; tells TypeScript that StringMap can have any number of properties, where the keys are strings, and the values associated with those keys are also strings. This is checked at compile time. If you try to assign a number to a property, TypeScript will flag it as an error.

Mapped Types

Mapped types, on the other hand, are a way to transform existing types into new types. They iterate over the properties of a type and apply a transformation.

Let’s say we have a type representing user preferences, and we want to create a new type where each preference can be marked as "pending."

type UserPreferences = {
  theme: "light" | "dark";
  fontSize: "small" | "medium" | "large";
  notifications: boolean;
};

// Mapped type to create a pending version
type PendingUserPreferences<T> = {
  [K in keyof T]?: T[K] | "pending"; // This is a mapped type
};

type CurrentPendingPreferences = PendingUserPreferences<UserPreferences>;

const prefs: CurrentPendingPreferences = {
  theme: "dark",
  fontSize: "pending",
  notifications: true,
};

const prefs2: CurrentPendingPreferences = {
  theme: "pending",
  fontSize: "medium",
  // notifications: "pending" // This would also be valid
};

console.log(prefs.theme);
console.log(prefs2.fontSize);

In PendingUserPreferences<T>, [K in keyof T] iterates over each key (K) in the original type T. For each key, it creates a new property with the same key (K), but its value is now T[K] | "pending". The ? makes the property optional, which is often useful in transformations but not strictly part of the "mapping" itself.

The Core Difference: Declaration vs. Transformation

The fundamental difference is that index signatures declare a pattern for how an object can be accessed, while mapped types transform an existing type definition.

  • Index Signatures: Are about the runtime structure and access patterns of an object. They are a direct declaration of what keys and values are allowed when you don’t have a fixed set of known keys. TypeScript uses them to ensure that any property you access on an object with an index signature will have the declared value type, and that any assignment to a property matches that type. They don’t inherently "know" about other types; they define a standalone property constraint.

  • Mapped Types: Are a compile-time type-level operation. They take an existing type (or a union of types, or a tuple type, etc.), iterate over its properties, and construct a new type based on those properties. They are powerful for generating related types, like making all properties optional, readonly, or transforming their values based on some logic. They are fundamentally about manipulating type definitions.

Think of it this way:

  • An index signature is like saying, "This box can hold any number of items, as long as each item is a string." You’re defining the rules for adding items to this specific box.

  • A mapped type is like saying, "Take all the items from Box A, and for each item, create a corresponding item in Box B that is twice as big." You’re transforming the contents of one box into another.

When to Use Which

  • Use Index Signatures when:

    • You’re dealing with data that inherently has dynamic keys, like configuration objects loaded from external sources, or dictionaries.
    • You need to enforce a consistent value type across a set of properties where the property names are not known beforehand.
    • You’re defining interfaces for APIs that return objects with arbitrary string keys.
  • Use Mapped Types when:

    • You need to derive a new type from an existing one by modifying its properties (e.g., making them optional, readonly, or changing their value types).
    • You want to generate utility types that operate on other types (e.g., Partial<T>, Readonly<T>, Record<K, T>).
    • You are performing complex type-level transformations and need to iterate over the keys of another type.

The Counter-Intuitive Part: Record<K, T> vs. Index Signatures

Many developers learn about Record<K, T> (a built-in mapped type) and think it’s a direct replacement for index signatures when they know the key type. For example, Record<'a' | 'b', string> or Record<string, string>. While Record<string, string> looks very similar to interface MyMap { [key: string]: string; }, there’s a subtle difference in how they are used and interpreted by TypeScript. An index signature is a property of an object type, directly describing its structure. Record<K, T> is a utility type that constructs an object type. When you use Record<string, string>, you’re creating a type that behaves like an object with an index signature, but it’s a result of a type-level computation rather than a direct declaration of an object’s shape. This distinction becomes more apparent when you start using more advanced conditional types or inferring types, where the origin of the type matters.

The next concept you’ll likely encounter is how these, particularly mapped types, are fundamental to creating generic utility types in TypeScript.

Want structured learning?

Take the full Typescript course →