Chapter 4.6☕ 14 min read

Index Signatures

When keys are dynamic but types are fixed

01The Pearl Treasury Catalog

Sometimes you don't know all the property names upfront, but you know the PATTERN — every key is a string, and every value is a specific type. Index signatures let you define types for objects with dynamic keys.

Think of the Nizam's Pearl Treasury at Laad Bazaar. The Nizam doesn't know the NAME of every single pearl that will ever enter the treasury — "Basra Pearl," "Gulf Pearl," "South Sea Pearl," "Tahiti Pearl" — new types keep arriving with every merchant ship. But he knows ONE rule: whatever the name, EVERY entry in the pearl catalog is a string (the pearl name), and its value is a number (the carat weight).

The specific keys are dynamic, unpredictable, and ever-growing, but the KEY TYPE and VALUE TYPE are fixed. That's exactly what an index signature expresses: [key: string]: number means "any string key maps to a number value." You don't need to know the exact key names at compile time.

Another analogy: the RTC bus route chart. You don't know every stop name that might ever be added to the network — new routes and stops appear all the time. But every stop (string key) has a fare (number value). The pattern is consistent even when the data is dynamic. Index signatures are TypeScript's way of saying "I don't know the exact keys, but I know the shape of every entry." This is essential for dictionaries, caches, configurations, and any data where the keys come from an external source like an API or a database.

02Index Signature Syntax

The basic syntax for an index signature is straightforward. You place it inside an object type or interface, using square brackets around the key name and type:

{ [key: string]: ValueType }

The key name is completely arbitrary — you can call it key, k, prop, index, or anything else. It doesn't change the behavior. The key TYPE must be string, number, or symbol — no other types are allowed.

Here is a practical example with city fares:

const fares: {
  [city: string]: number;
} = {
  Mumbai: 500,
  Delhi: 800,
  Chennai: 400,
};

And an environment variable configuration:

const env: {
  [key: string]: string;
} = {
  NODE_ENV: "production",
  PORT: "3000",
};

The CRITICAL rule: if you have an index signature, EVERY property's value type must be compatible with it. This will error:

const scores: {
  [id: string]: number;
} = {
  math: 90,
  science: 85,
  name: "Imran", // ERROR!
};

"name" is a string, but the index signature says all values must be number. TypeScript enforces this strictly because someone might access scores["name"] and expect a number.

You can use index signatures in interfaces, including with generics:

interface Dictionary<T> {
  [key: string]: T;
}

And there's a cleaner alternative utility type (covered fully in Stage 7):

// These are equivalent:
Record<string, number>
{ [key: string]: number }
03Named + Dynamic Properties

You CAN have specific named properties alongside an index signature — but ALL named property values must be compatible with the index signature's value type. This is where many developers trip up.

Here is a valid example — both theme (string) and notifications (boolean) are compatible with string | boolean:

interface UserPrefs {
  [key: string]: string | boolean;
  theme: string;
  notifications: boolean;
}

Here is an INVALID example — count is number, but the index signature says all values are string:

interface Bad {
  [key: string]: string;
  count: number; // ERROR!
}

This fails because someone could access obj["count"] through the index signature, which would claim it returns string, but the actual value is number. TypeScript won't allow this inconsistency.

The fix is to widen the index signature to include number:

interface Fixed {
  [key: string]: string | number;
  count: number; // Now valid!
}

A common real-world pattern where named properties work perfectly is environment configuration, where all values are strings:

interface EnvConfig {
  [key: string]: string;
  API_KEY: string;
  BASE_URL: string;
}

Number keys are also valid for array-like objects:

const arrLike: {
  [index: number]: string;
} = { 0: "a", 1: "b", 2: "c" };

But remember: JavaScript converts number keys to strings internally, so { 0: "a" } actually has the key "0". TypeScript handles this automatically, but it's worth knowing when debugging.

04Index Signature Traps

Index signatures come with several traps and common mistakes that can undermine your type safety if you're not careful.

Trap 1: Index signature too broad. Using { [key: string]: any } defeats the entire purpose of TypeScript! You lose all type safety on the values. Avoid any in index signatures. Use specific types or generics instead.

// BAD - loses all safety
const data: { [k: string]: any } = {};

// GOOD - specific value type
const data: { [k: string]: number } = {};

Trap 2: Readonly index signatures. You can make an index signature readonly to prevent writes while allowing reads. This is useful for frozen configurations:

interface FrozenConfig {
  readonly [key: string]: string;
}

Trap 3: Template literal index signatures. TypeScript 4.4+ allows template literal patterns as index signatures:

interface DataAttrs {
  [key: `data-${string}`]: string;
}

Only keys matching the pattern like data-id or data-name are valid.

Trap 4: Symbol keys. { [key: symbol]: number } is valid but rare. Useful for internal metadata.

Trap 5: The "excess property" gap. Index signatures BYPASS excess property checking! With a normal type, extra properties cause errors. With an index signature, ANY matching key-value pair is silently accepted. This can hide bugs:

interface Loose {
  [k: string]: number;
}
const obj: Loose = { a: 1, b: 2, oops: "x" };
// Error caught, but only because value
// type is wrong. Extra keys with right
// type pass silently!

Trap 6: Don't use index signatures when you know all the keys. If you know it's exactly { name, age, email }, type those explicitly. Index signatures are for TRULY dynamic data where keys come from external sources.

05Index Signatures Cheatsheet

Let us recap everything with a clean cheatsheet for index signatures.

Basic Syntax:

{ [key: string]: ValueType }

With Interface:

interface Dict {
  [k: string]: number;
}

With Named Properties:

interface X {
  [k: string]: string;
  name: string; // must match!
}

Generic Dictionary:

interface Dict<T> {
  [k: string]: T;
}

Number Keys (Array-like):

{ [i: number]: string }

Readonly Index Signature:

{ readonly [k: string]: T }

Cleaner Alternative:

Record<string, T>
// Same as { [k: string]: T }

Key Rules to Remember:

  • All named property values MUST be compatible with the index signature value type — no exceptions.
  • Key type must be string, number, or symbol — no unions, no other types.
  • Avoid [key: string]: any — it is too broad and defeats type safety entirely.
  • Use index signatures for truly dynamic keys, not as a shortcut when you know all the property names.
  • Record<K, V> is often a cleaner alternative for simple dictionaries.

The golden rule: "Index signatures are the Nizam's treasury catalog — you don't know every pearl's name, but you know they're all measured in carats. Use them for dynamic keys, not as a shortcut for proper typing, bhai!"

Key Takeaways

  • Index signatures define types for objects with dynamic keys: `[key: string]: ValueType`
  • All named property values MUST be compatible with the index signature value type
  • Key type must be `string`, `number`, or `symbol` only
  • Avoid `[key: string]: any` — it destroys type safety
  • Use `Record` as a cleaner alternative for simple dictionaries
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase