Skip to content

24 Index Signatures

DodaTech 7 min read

In this tutorial, you will learn about TypeScript Index Signatures. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript index signatures let you define objects with dynamic property names — specifying the type for all properties when you don't know their names in advance, enabling type-safe dictionaries, caches, and configuration maps.

What You'll Learn

  • Index signature syntax: [key: string]: T and [key: number]: T
  • Mixing named and dynamic properties
  • The Record<K, V> utility type
  • Readonly index signatures
  • Practical patterns for dictionaries and maps

Why It Matters

Real-world data often has dynamic keys — HTTP headers, environment variables, API responses, Caching layers. Index signatures let you type these structures without sacrificing safety or resorting to any.

Real-World Use

The Doda Browser extension API uses index signatures for browser.storage.local — a key-value store where keys are strings and values can be any JSON-serializable type. Durga Antivirus Pro uses Record<string, ThreatSummary> for its threat aggregation cache.

Learning Path

flowchart LR
  A[This Typing] --> B[Index Signatures]
  B --> C[Type Guards]
  B --> D[You Are Here]
  C --> E[Narrowing]
  E --> F[Recursive Types]

String Index Signature

interface StringDictionary {
  [key: string]: string;
}

const env: StringDictionary = {
  NODE_ENV: "production",
  API_KEY: "sk-abc123",
  DB_HOST: "localhost",
};

// Access is typed as string
const nodeEnv: string = env.NODE_ENV;

// env.PORT = 3000; // Error: Type 'number' is not assignable to type 'string'

Think of an index signature like a blanket rule: "All properties of this object must conform to this type."

Number Index Signature

interface NumberDictionary {
  [index: number]: string;
}

const days: NumberDictionary = {
  0: "Sunday",
  1: "Monday",
  2: "Tuesday",
};

console.log(days[1]); // Monday

Arrays use number index signatures:

interface TypedArray<T> {
  [index: number]: T;
  length: number;
  push(item: T): void;
  // ...
}

Mixing Named and Index Properties

When you have a mix of known and unknown properties:

interface HttpResponse {
  status: number;
  statusText: string;
  headers: { [key: string]: string };
  // Known properties
  ok: boolean;
  // Dynamic data
  [key: string]: unknown;
}

const response: HttpResponse = {
  status: 200,
  statusText: "OK",
  ok: true,
  headers: {
    "Content-Type": "application/json",
    "X-Request-Id": "req-abc",
  },
  // Dynamic extra data
  data: { userId: 1 },
  meta: { page: 1 },
};

Constraint: All Named Properties Must Match the Index Signature

interface Config {
  [key: string]: string;
  // port: number; // Error: Property 'port' of type 'number' is not assignable to string index type
  port: string; // OK — matches the index signature
}

Readonly Index Signatures

Prevent modification of dynamic properties:

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

const config: ReadonlyMap<string> = {
  apiUrl: "https://api.example.com",
  version: "1.0",
};

// config.apiUrl = "new-url"; // Error: Index signature in type 'ReadonlyMap<string>' only permits reading

Optional Index Signature Members

interface LooseConfig {
  [key: string]: string | undefined;
}

const config: LooseConfig = {
  host: "localhost",
  // Not all keys must be present
};

// Access returns string | undefined
const host = config.host; // string | undefined
const missing = config.port; // string | undefined — no error

The Record<K, V> Utility

Record<K, V> is a built-in mapped type that creates an object type with keys K and values V:

// type Record<K extends keyof any, V> = { [P in K]: V };

type PageNames = "home" | "about" | "contact";
type PageMap = Record<PageNames, { title: string; path: string }>;

const pages: PageMap = {
  home: { title: "Home", path: "/" },
  about: { title: "About", path: "/about" },
  contact: { title: "Contact", path: "/contact" },
};

Record vs Index Signature

Aspect Index Signature Record<K, V>
Keys Any string/number Specific union of literals
Type safety Global rule Per-key check
Autocompletion None Full autocompletion for known keys
Use case Dynamic dictionaries Known key sets

Practical Patterns

Type-Safe Caching

interface CacheEntry<T> {
  value: T;
  expiresAt: number;
}

class TypedCache<T> {
  private store: { [key: string]: CacheEntry<T> } = {};

  get(key: string): T | undefined {
    const entry = this.store[key];
    if (!entry) return undefined;
    if (Date.now() > entry.expiresAt) {
      delete this.store[key];
      return undefined;
    }
    return entry.value;
  }

  set(key: string, value: T, ttlMs: number = 60000): void {
    this.store[key] = {
      value,
      expiresAt: Date.now() + ttlMs,
    };
  }

  clear(): void {
    this.store = {};
  }
}

const scanCache = new CacheEntry<{ id: string; threats: string[] }>();
scanCache.set("scan-001", { id: "scan-001", threats: ["Trojan"] });

HTTP Headers

type Headers = Record<string, string | string[] | undefined>;

function setHeader(headers: Headers, name: string, value: string): Headers {
  return { ...headers, [name.toLowerCase()]: value };
}

function getHeader(headers: Headers, name: string): string | undefined {
  const value = headers[name.toLowerCase()];
  return Array.isArray(value) ? value[0] : value;
}

const headers: Headers = {
  "content-type": "application/json",
  "x-request-id": "req-001",
};

console.log(getHeader(headers, "Content-Type")); // application/json

Enum-Like Maps

const ERROR_MESSAGES: Record<number, string> = {
  400: "Bad Request",
  401: "Unauthorized",
  403: "Forbidden",
  404: "Not Found",
  500: "Internal Server Error",
};

function getErrorMessage(code: number): string {
  return ERROR_MESSAGES[code] ?? "Unknown Error";
}

console.log(getErrorMessage(404)); // Not Found
console.log(getErrorMessage(418)); // Unknown Error

Common Mistakes

1. Using Index Signature When Specific Keys Are Known

// Bad: lose autocompletion
interface Config {
  [key: string]: string;
}

// Good: use specific keys
interface Config {
  host: string;
  port: string;
  debug: string;
}

2. Forgetting That Named Properties Must Conform to the Index

interface Dictionary {
  [key: string]: string;
  length: number; // Error: number not assignable to string
}

3. Accessing Properties That Might Not Exist

interface Options {
  [key: string]: string | undefined;
}

const opts: Options = {};
// opts.debug; // type: string | undefined — must check for undefined

4. Using Index Signatures for Arrays

Use Array<T> or T[] instead of number-indexed signatures.

5. Not Using readonly for Immutable Dictionaries

If the dictionary shouldn't change after creation, use readonly on the index signature.

6. Confusing Record<K, V> with Index Signatures

Record<string, V> is a mapped type equivalent to { [key: string]: V }. Record<"a" | "b", V> lists specific keys.

Practice Questions

  1. What does [key: string]: number mean in an interface? All string-keyed properties must have number values.

  2. Can you have both an index signature and named properties? Yes, but named properties must be assignable to the index signature's value type.

  3. What is the Record<K, V> utility type? A mapped type that creates an object with keys from K and values of type V.

  4. How do you make an index signature read-only? readonly [key: string]: T — all properties become read-only.

Challenge: Create a type-safe EventBus that uses index signatures to map event names to arrays of handler functions. Each event can have different payload types.

FAQ

What is the difference between `[key: string]: T` and `Record`?

They are equivalent. Record is syntax sugar for a mapped type over the keys union.

Can an index signature have both string and number keys?

Yes: [key: string | number]: T. JavaScript converts number keys to strings anyway.

What happens if I access a non-existent key on an index-signed object?

TypeScript infers the value type (which might include undefined if the signature allows it). At runtime, you get undefined.

Can I use symbols as index signature keys?

Yes: [key: symbol]: T.

What is `keyof any`?

It's string | number | symbol — all types that can be used as object keys.

Mini Project: Configuration Store

// src/config-store.ts

type ConfigValue = string | number | boolean | ConfigValue[] | { [key: string]: ConfigValue };

interface ConfigStore {
  [namespace: string]: {
    [key: string]: ConfigValue;
  };
}

class AppConfig {
  private store: ConfigStore = {};

  set(namespace: string, key: string, value: ConfigValue): void {
    if (!this.store[namespace]) {
      this.store[namespace] = {};
    }
    this.store[namespace][key] = value;
  }

  get(namespace: string, key: string): ConfigValue | undefined {
    return this.store[namespace]?.[key];
  }

  getNamespace(namespace: string): Record<string, ConfigValue> | undefined {
    return this.store[namespace];
  }

  getAll(): Readonly<ConfigStore> {
    return this.store;
  }

  clear(): void {
    this.store = {};
  }
}

const config = new AppConfig();

config.set("app", "name", "DodaTech Scanner");
config.set("app", "version", "2.1.0");
config.set("app", "debug", false);

config.set("database", "host", "localhost");
config.set("database", "port", 5432);
config.set("database", "ssl", true);

config.set("features", "realTimeScanning", true);
config.set("features", "maxFileSize", 10485760);

console.log(config.get("app", "name"));      // DodaTech Scanner
console.log(config.get("database", "port")); // 5432
console.log(config.getAll());
// {
//   app: { name: "DodaTech Scanner", version: "2.1.0", debug: false },
//   database: { host: "localhost", port: 5432, ssl: true },
//   features: { realTimeScanning: true, maxFileSize: 10485760 }
// }

What's Next

You've completed Module 3: Classes & OOP. Now dive into Type System Deep Dive:

Lesson Description
{{< ref "/programming-languages/typescript/23-this-typing" >}} Review this typing
{{< ref "/programming-languages/typescript/25-type-guards" >}} Type guards and discriminated unions
{{< ref "/programming-languages/typescript/26-narrowing" >}} Control flow analysis and narrowing

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro