Back to Blog
TypeScriptIndex SignaturesType SafetyObjects

TypeScript Index Signatures Guide

Master TypeScript index signatures for typing objects with dynamic keys and dictionary patterns.

B
Bootspring Team
Engineering
May 11, 2019
6 min read

Index signatures allow typing objects with dynamic keys. Here's how to use them effectively.

Basic Syntax#

1// String index signature 2interface StringDictionary { 3 [key: string]: string; 4} 5 6const dict: StringDictionary = { 7 name: 'John', 8 city: 'New York', 9 // age: 30, // Error: number not assignable to string 10}; 11 12// Number index signature 13interface NumberDictionary { 14 [index: number]: string; 15} 16 17const arr: NumberDictionary = { 18 0: 'first', 19 1: 'second', 20 // 'key': 'value', // Error: string key not allowed 21};

With Known Properties#

1// Combine fixed and dynamic properties 2interface User { 3 id: string; 4 name: string; 5 [key: string]: string; // All other properties must be string 6} 7 8const user: User = { 9 id: '123', 10 name: 'John', 11 email: 'john@example.com', 12 role: 'admin', 13}; 14 15// Mixed types require union 16interface Config { 17 version: number; 18 [key: string]: string | number; 19} 20 21const config: Config = { 22 version: 1, 23 apiUrl: 'https://api.example.com', 24 timeout: 5000, 25};

Record Type#

1// Record is a cleaner alternative 2type StringRecord = Record<string, string>; 3 4// Equivalent to: 5type StringRecord2 = { 6 [key: string]: string; 7}; 8 9// With specific keys 10type Roles = 'admin' | 'user' | 'guest'; 11type RolePermissions = Record<Roles, string[]>; 12 13const permissions: RolePermissions = { 14 admin: ['read', 'write', 'delete'], 15 user: ['read', 'write'], 16 guest: ['read'], 17};

Mapped Types vs Index Signatures#

1// Index signature - any string key 2interface AnyKeys { 3 [key: string]: number; 4} 5 6// Mapped type - specific keys 7type SpecificKeys = { 8 [K in 'a' | 'b' | 'c']: number; 9}; 10// { a: number; b: number; c: number } 11 12// Partial mapped type 13type PartialRecord<K extends string, V> = { 14 [P in K]?: V; 15}; 16 17type OptionalFlags = PartialRecord<'debug' | 'verbose', boolean>; 18// { debug?: boolean; verbose?: boolean }

Generic Dictionaries#

1// Generic dictionary type 2interface Dictionary<T> { 3 [key: string]: T; 4} 5 6const numberDict: Dictionary<number> = { 7 one: 1, 8 two: 2, 9}; 10 11const arrayDict: Dictionary<string[]> = { 12 fruits: ['apple', 'banana'], 13 colors: ['red', 'blue'], 14}; 15 16// Function using dictionary 17function getValue<T>(dict: Dictionary<T>, key: string): T | undefined { 18 return dict[key]; 19}

Safe Index Access#

1// Index access can return undefined 2interface SafeDict { 3 [key: string]: string; 4} 5 6const dict: SafeDict = { a: 'value' }; 7 8// With noUncheckedIndexedAccess: true 9const value = dict['b']; // string | undefined 10 11// Check before use 12if (value !== undefined) { 13 console.log(value.toUpperCase()); 14} 15 16// Or use optional chaining 17console.log(dict['b']?.toUpperCase());

Template Literal Keys#

1// Dynamic key patterns with template literals 2type EventHandler<T extends string> = { 3 [K in `on${Capitalize<T>}`]: () => void; 4}; 5 6type ClickHandler = EventHandler<'click'>; 7// { onClick: () => void } 8 9// Multiple events 10type Events = 'click' | 'focus' | 'blur'; 11type AllHandlers = EventHandler<Events>; 12// { 13// onClick: () => void; 14// onFocus: () => void; 15// onBlur: () => void; 16// }

Readonly Index Signatures#

1// Readonly dictionary 2interface ReadonlyDict { 3 readonly [key: string]: string; 4} 5 6const frozen: ReadonlyDict = { 7 a: '1', 8 b: '2', 9}; 10 11// frozen['a'] = '3'; // Error: Index signature is readonly 12 13// With Readonly utility 14type FrozenRecord = Readonly<Record<string, number>>; 15 16const frozenNumbers: FrozenRecord = { x: 1, y: 2 }; 17// frozenNumbers['x'] = 3; // Error

Discriminated Index Types#

1// Different value types based on key pattern 2type Config = { 3 [K in `${string}Count`]: number; 4} & { 5 [K in `${string}Name`]: string; 6} & { 7 [K in `${string}Enabled`]: boolean; 8}; 9 10const config: Config = { 11 userCount: 100, 12 userName: 'admin', 13 userEnabled: true, 14 itemCount: 50, 15 itemName: 'widget', 16 itemEnabled: false, 17};

Object.keys and Index Signatures#

1interface Person { 2 name: string; 3 age: number; 4} 5 6const person: Person = { name: 'John', age: 30 }; 7 8// Object.keys returns string[] 9Object.keys(person).forEach((key) => { 10 // key is string, not keyof Person 11 // person[key]; // Error without index signature 12}); 13 14// Type-safe iteration 15(Object.keys(person) as Array<keyof Person>).forEach((key) => { 16 console.log(person[key]); // OK 17}); 18 19// Or use Object.entries 20Object.entries(person).forEach(([key, value]) => { 21 console.log(`${key}: ${value}`); 22});

Symbol Index Signatures#

1// Symbol keys 2interface SymbolKeyed { 3 [key: symbol]: string; 4} 5 6const sym = Symbol('key'); 7const obj: SymbolKeyed = { 8 [sym]: 'value', 9}; 10 11console.log(obj[sym]); // 'value' 12 13// Multiple index signature types 14interface MixedIndex { 15 [key: string]: string | number; 16 [key: number]: number; // Must be compatible with string indexer 17 [key: symbol]: string; 18}

Class with Index Signature#

1class Cache<T> { 2 [key: string]: T | ((key: string) => T | undefined) | ((key: string, value: T) => void); 3 4 private data: Record<string, T> = {}; 5 6 get(key: string): T | undefined { 7 return this.data[key]; 8 } 9 10 set(key: string, value: T): void { 11 this.data[key] = value; 12 } 13} 14 15// Cleaner approach with private storage 16class BetterCache<T> { 17 #data: Map<string, T> = new Map(); 18 19 get(key: string): T | undefined { 20 return this.#data.get(key); 21 } 22 23 set(key: string, value: T): void { 24 this.#data.set(key, value); 25 } 26}

Validation with Index Signatures#

1// Validate object shape 2function validateRecord<T>( 3 obj: Record<string, unknown>, 4 validator: (value: unknown) => value is T 5): obj is Record<string, T> { 6 return Object.values(obj).every(validator); 7} 8 9function isString(value: unknown): value is string { 10 return typeof value === 'string'; 11} 12 13const data: Record<string, unknown> = { a: 'hello', b: 'world' }; 14 15if (validateRecord(data, isString)) { 16 // data is Record<string, string> 17 Object.values(data).forEach((v) => console.log(v.toUpperCase())); 18}

Best Practices#

When to Use: ✓ Dynamic object keys ✓ Dictionary/map patterns ✓ Configuration objects ✓ Cache implementations Safety: ✓ Enable noUncheckedIndexedAccess ✓ Check for undefined ✓ Use Map for runtime safety ✓ Prefer Record<K, V> syntax Patterns: ✓ Combine with known properties ✓ Use template literal keys ✓ Consider readonly ✓ Use generics for flexibility Avoid: ✗ Overly permissive signatures ✗ Ignoring undefined returns ✗ Complex nested signatures ✗ When specific keys are known

Conclusion#

Index signatures type objects with dynamic keys, enabling dictionary patterns and flexible configurations. Use [key: string]: Type for string keys, combine with known properties when needed, and prefer Record<K, V> for cleaner syntax. Enable noUncheckedIndexedAccess for safety, as index access can return undefined. For runtime type safety with dynamic keys, consider using Map instead of plain objects.

Share this article

Help spread the word about Bootspring