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; // ErrorDiscriminated 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.