24 Index Signatures
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]: Tand[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
What does
[key: string]: numbermean in an interface? All string-keyed properties must have number values.Can you have both an index signature and named properties? Yes, but named properties must be assignable to the index signature's value type.
What is the
Record<K, V>utility type? A mapped type that creates an object with keys fromKand values of typeV.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
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