TypeScript Index Signatures
An index signature lets you describe an object whose property names you do not know in advance, while still saying what type every value must have.
Understanding Index Signatures in TypeScript
Index signatures give you a way to type objects with dynamic property names without losing type safety.
They tell TypeScript what type of value comes back when you read a property with bracket notation (obj[key]), even when the exact property names are not known ahead of time.
Key ideas:
- Dynamic property access: handle objects with any property names.
- Type safety: every dynamic property must hold the same kind of value.
- Flexible data structures: model dictionaries, maps and other lookup tables.
- Early errors: mistakes with value types are caught at compile time.
Basic Index Signatures
String index signatures. Use an index signature when you do not know the property names in advance, but you do know the shape of the values.
The syntax uses brackets: [key: type] describes the type of the property names (the keys), and after the colon comes the type of the values those properties can hold.
Number index signatures. TypeScript supports number keys as well as string keys.
Note: In JavaScript, all object keys are stored as strings, even numeric ones. TypeScript still tells the two apart to help catch logical errors when you mix up arrays and objects.
String index signature
StringDictionary accepts any string key, but every value must be a string. Reading, adding and updating keys all work, while assigning a number is an error.
StringDictionary whose keys are strings and whose values are strings.
Create a names object of that type with firstName: "Alice", lastName: "Smith" and the key "100" mapped to "One Hundred", log the three values with bracket access, then add an age key with the string "30". Leave a commented-out line showing that assigning the number 30 would be a type error.
Output: TypeScript file and captured output
// This interface represents an object with string keys and string values
interface StringDictionary {
[key: string]: string;
}
// Creating a compliant object
const names: StringDictionary = {
firstName: "Alice",
lastName: "Smith",
"100": "One Hundred"
};
// Accessing properties
console.log(names["firstName"]); // "Alice"
console.log(names["lastName"]); // "Smith"
console.log(names["100"]); // "One Hundred"
// Adding new properties dynamically
names["age"] = "30";
// This would cause an error
// names["age"] = 30; // Error: Type 'number' is not assignable to type 'string'
Number index signature
NumberDictionary uses number keys. Because the value type is any, each entry can hold a different kind of value.
NumberDictionary whose keys are numbers and whose values are any.
Create a scores object with 0: "Zero", 1: 100 and 2: true, log the three entries by numeric index, then add index 3 holding the object { passed: true }.
Output: TypeScript file and captured output
// Object with number indexes
interface NumberDictionary {
[index: number]: any;
}
const scores: NumberDictionary = {
0: "Zero",
1: 100,
2: true
};
console.log(scores[0]); // "Zero"
console.log(scores[1]); // 100
console.log(scores[2]); // true
// Adding a complex object
scores[3] = { passed: true };
Advanced Index Signature Patterns
Mixed property types. You can combine an index signature with explicitly named properties. The named properties are required; everything else must match the index signature.
Important: When you mix explicit properties with an index signature, the type of every explicit property must be assignable to the index signature's value type.
Readonly index signatures. Add readonly to an index signature to stop values from being changed after the object is created.
For constraining key sets and transforming shapes, see the Mapped Types lesson.
Mixed property types
UserInfo requires name and age, and allows any other property as long as its value is a string or a number.
UserInfo interface: required name: string and age: number, plus a [key: string]: string | number signature for any other property.
Create a valid user with name: "Alice", age: 30, address: "123 Main St" and zipCode: 12345. Add a commented-out invalidUser showing that age: "thirty" and isAdmin: true would each be type errors. The file prints nothing.
Output: TypeScript file and captured output
interface UserInfo {
name: string; // Required property with specific name
age: number; // Required property with specific name
[key: string]: string | number; // All other properties must be string or number
}
const user: UserInfo = {
name: "Alice", // Required
age: 30, // Required
address: "123 Main St", // Optional
zipCode: 12345 // Optional
};
// This would cause an error
// const invalidUser: UserInfo = {
// name: "Bob",
// age: "thirty", // Error: Type 'string' is not assignable to type 'number'
// isAdmin: true // Error: Type 'boolean' is not assignable to type 'string | number'
// };
Readonly index signature
Reading names[0] is fine, but writing to any index of a ReadOnlyStringArray is a compile error.
ReadOnlyStringArray interface with a readonly [index: number]: string index signature.
Assign the array ["Alice", "Bob", "Charlie"] to a names variable of that type, log names[0], and leave a commented-out assignment to names[0] explaining that writing through a readonly index signature is a type error.
Output: TypeScript file and captured output
interface ReadOnlyStringArray {
readonly [index: number]: string;
}
const names: ReadOnlyStringArray = ["Alice", "Bob", "Charlie"];
console.log(names[0]); // "Alice"
// This would cause an error
// names[0] = "Andrew"; // Error: Index signature in type 'ReadOnlyStringArray' only permits reading
Real-World Examples
API response handling. Index signatures are handy for API responses where the resource names and extra metadata fields are not fixed.
API response handling
ApiResponse<T> lets data hold any resource name mapped to an array of T, and lets meta carry extra fields beyond page and total.
ApiResponse<T> interface whose data maps any resource name ([resourceType: string]) to T[], and whose meta has page: number, total: number plus a [key: string]: any signature for extra metadata.
Declare a User interface (id, name, email), build an apiResponse: ApiResponse<User> with two users (Alice and Bob) and meta page: 1, total: 2, timestamp: "2023-01-01T00:00:00Z", then log the first user's name.
Output: TypeScript file and captured output
// Type for API responses with dynamic keys
interface ApiResponse<T> {
data: {
[resourceType: string]: T[]; // e.g., { "users": User[], "posts": Post[] }
};
meta: {
page: number;
total: number;
[key: string]: any; // Allow additional metadata
};
}
// Example usage with a users API
interface User {
id: number;
name: string;
email: string;
}
// Mock API response
const apiResponse: ApiResponse<User> = {
data: {
users: [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" }
]
},
meta: {
page: 1,
total: 2,
timestamp: "2023-01-01T00:00:00Z"
}
};
// Accessing the data
const users = apiResponse.data.users;
console.log(users[0].name); // "Alice"
Best Practices
Do:
- Use index signatures for collections with dynamic keys.
- Combine them with explicit properties for the fields you do know.
- Keep value types specific — avoid
any. - Use
readonlywhen the values should not change.
Don't:
- Reach for an index signature when the keys are known — prefer a fixed interface.
- Forget that every property, including explicit ones, must fit the index signature's value type.
- Reinvent mapped types — use them for shape transformations.
Common Pitfalls
A common mistake is declaring an explicit property whose type does not fit the index signature. Widen the index signature's value type to fix it.
Conflicting property and index types
ConflictingTypes fails because name: string does not fit the number index signature; FixedTypes widens the value type to number | string so both properties are allowed.
interface ConflictingTypes {
[key: string]: number;
name: string; // Error: not assignable to string index type 'number'
}
interface FixedTypes {
[key: string]: number | string;
name: string; // OK
age: number; // OK
}
Index Signatures vs Record
Use an index signature when the keys are flexible or dynamic, and when you also need to mix in other properties.
Use Record<K, T> for concise, simple mappings from one key type to one value type.
Index signature and Record side by side
StringMap and StringRecord describe the same shape — any string key mapped to a string value — written two different ways.
// Index signature
interface StringMap {
[key: string]: string;
}
// Record
type StringRecord = Record<string, string>;
Conclusion
Use index signatures when keys are dynamic but the value shape is consistent.
Combine them with explicit properties where possible, and see the Mapped Types and Utility Types lessons for more advanced transformations.