Chapter 32: TypeScript Index Signatures
Index Signatures in TypeScript very slowly and clearly, like we’re sitting together with VS Code open, chai on the table, and we’re typing small examples one by one until you really feel comfortable with them.
Index signatures are one of the most misunderstood but most frequently used features when dealing with real-world data — especially API responses, configuration objects, dictionaries, dynamic keys, form data, etc.
1. What is an index signature? (the simplest honest explanation)
An index signature tells TypeScript:
“This object can have any number of properties whose names are strings (or numbers or symbols), and the values of those properties must be of a specific type.”
In other words — it’s how you type objects with dynamic / unknown keys.
Without index signature → TypeScript only allows the properties you explicitly wrote.
With index signature → TypeScript allows extra properties (as long as their values match the declared type).
2. Basic syntax — the most common form
|
0 1 2 3 4 5 6 7 8 |
interface StringDictionary { [key: string]: string; } |
- [key: string] → the index (property name) must be string
- : string → the value of any property must be string
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const dict: StringDictionary = { name: "Sara", city: "Hyderabad", language: "Telugu" // you can add as many string keys as you want }; // dict.age = 28; → ❌ Error — number is not string dict.country = "India"; // ✅ OK |
3. Real-world examples you see every day (2025–2026)
Example 1: API response with dynamic fields
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
interface EnvVars { [key: string]: string | undefined; } const env: EnvVars = process.env; // Very common pattern const apiKey = env.API_KEY; // string | undefined const port = env.PORT; // string | undefined // You can access any env variable without TypeScript complaining |
Example 2: String → number lookup table
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
interface ScoresBySubject { [subject: string]: number; } const studentScores: ScoresBySubject = { maths: 92, physics: 85, chemistry: 78, // can add more subjects later }; |
Example 3: Form values / uncontrolled inputs
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
interface FormData { [fieldName: string]: string | File | null; } function handleSubmit(data: FormData) { // data.email → string | File | null // data.resume → string | File | null // data["custom-1"] → also allowed } |
Example 4: CSS-in-JS style objects / theme partials
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
interface StyleOverrides { [className: string]: React.CSSProperties; } const customStyles: StyleOverrides = { ".btn-primary": { backgroundColor: "#3b82f6" }, ".card": { borderRadius: "12px" }, // can add more selectors dynamically }; |
4. Number index signatures (less common but useful)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
interface NumberToStringMap { [index: number]: string; } const codes: NumberToStringMap = { 200: "OK", 404: "Not Found", 500: "Server Error" }; // codes[200] → string // codes["200"] → also works (JS coerces to number) |
Very common when dealing with:
- HTTP status code → message mappings
- Array-like objects with numeric keys
- Sparse arrays / data grids
5. Combining explicit properties + index signature
This is the most realistic pattern — you want some known properties and allow extra ones.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
interface ApiResponse { success: boolean; message?: string; data?: unknown; // allow any extra fields (very common in real APIs) [key: string]: any; // ← most permissive // or stricter: // [key: string]: unknown; // or even more precise: // [key: string]: string | number | boolean | null; } |
Important warning 2026 — many people now prefer unknown or union types over any in index signatures.
6. Index signatures vs mapped types (quick comparison)
| Feature / Goal | Index Signature | Mapped Type (better in most cases) |
|---|---|---|
| Allow any string keys | Yes | Yes — but usually more controlled |
| Syntax | [key: string]: string | { [K in string]: string } or Record<string, string> |
| Can combine with known properties | Yes — but with any risk | Yes — and more precise |
| Preserves literal key types | No — all keys widen to string | Yes — if using keyof or literals |
| Modern preference (2025–2026) | Used when keys are truly dynamic | Preferred when keys are known or union of literals |
Modern replacement for loose index signatures — Record utility
|
0 1 2 3 4 5 6 7 8 9 10 |
// Instead of [key: string]: string type Dict = Record<string, string>; // Even better — when keys are limited type StatusMessages = Record<200 | 404 | 500, string>; |
7. Common gotchas & modern best practices (2026 edition)
Gotcha 1 — Accessing known keys loses type safety
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
interface UserMap { [id: string]: { name: string; age: number }; } const users: UserMap = { "user-1": { name: "Rahul", age: 28 } }; const name = users["user-1"].name; // string — good const age = users["user-1"].age; // number — good // but: const unknownUser = users["random-key"]; // {name: string; age: number} | undefined unknownUser?.name // string | undefined — safe |
Gotcha 2 — No literal key safety without mapped types
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
const colors: { [key: string]: string } = { primary: "#3b82f6" }; colors.primary // string (widened) colors["primary"] // string colors.oops // string — no error! |
Modern fix — use Record + as const or mapped types
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
const colors = { primary: "#3b82f6", danger: "#ef4444" } as const satisfies Record<string, string>; colors.primary // "#3b82f6" (literal!) colors.danger // "#ef4444" // colors.oops → error |
Best practice 2026 — prefer:
- Record<string, T> or Record<UnionOfKeys, T> when possible
- Mapped types with keyof when keys are known
- Index signatures only when keys are truly dynamic and unknown at compile time
8. Quick cheat-sheet table
| Use-case | Recommended way 2026 | Example syntax |
|---|---|---|
| Truly dynamic keys (API, env, forms) | Index signature or Record<string, T> | [key: string]: unknown |
| Known but many keys | Record<Union | string, T> or as const | Record<“success” | “error”, string> |
| String → anything lookup | Record<string, any> (avoid any if possible) | Record<string, unknown> |
| Number keys (status codes, sparse) | [index: number]: T | [code: number]: string |
| Extra fields on known object | Known properties + index signature (carefully) | { id: number; [key: string]: unknown } |
Your mini homework (try in playground)
- Create type Translations = { [key: string]: string }
- Add English, Telugu, Hindi keys
- Try accessing translations.french → see it’s allowed but undefined
- Change to Record<“en” | “te” | “hi”, string> → see safety improve
- Try an index signature with known properties + extra dynamic ones
Which part feels most useful or most confusing for your current code?
Want to go deeper into:
- Index signatures vs mapped types vs Record<T>
- Real API response typing with dynamic fields
- Combining index signatures with generics
- Common mistakes in form / env / style objects
Just tell me — we’ll zoom right in! 😄
