TypeScript Index Signatures: 4 Examples Type-Safe Dynamic Objects
Alex
Posted on March 13, 2024
In TypeScript, index signatures are a powerful feature that allows you to type objects with unknown structures.
They are especially useful when you want to define a type for objects with unknown properties or when you want to create a dictionary-like data structure.
Additionally, index signatures are often used to create complex utility types that can be used to manipulate and transform other types.
What are Index Signatures?
An index signature defines a type for the values that an object can have at a particular index (key). It specifies a contract between the keys and the values of an object.
interface MyInterface {
[key: string]: number;
}
In this example, the index signature [key: string]
defines that any key of type string
will have a corresponding value of type number
.
This means that any object implementing the MyInterface
interface can have any number of string keys, and the values associated with those keys must be numbers.
Example 1: String-to-String Dictionary
Let’s say you want to create a simple dictionary that maps language codes (like “en”, “fr”, “es”) to their full English names (“English”, “French”, “Spanish”). With an index signature, you can define a type for this dictionary that allows for any number of language codes as keys, but ensures that all values are strings.
Define the Dictionary interface:
interface LanguageDictionary {
[key: string]: string;
}
Now we are sure that any string can be used as a key and all values are also strings:
const languages: LanguageDictionary = {
en: "English",
fr: "French",
es: "Spanish",
// You can add more languages without changing the type
};
// Adding a new language to the dictionary
languages.de = "German";
// Retrieving a language name
console.log(languages.en); // Output: "English"
Example 2: Product Inventory Object
Suppose you are building an e-commerce application and you want to represent a product inventory object that has a fixed set of properties (name, price) and a dynamic set of properties (stock for different sizes).
You can use an index signature to define a type for this object that allows for a fixed set of properties and a dynamic set of properties.
To maintain strict type safety without mixing specific properties with index signatures when their types differ, it’s better to separate the concerns.
// Bad example of mixing properties with index signatures
type BadProduct = {
name: string;
price: number;
// Error - Property 'name' of type 'string' is not assignable to 'string' index type 'number'
[size: string]: number;
}
// Better solution - It's immediately clear which parts of the object are fixed and which parts are dynamic.
type Product = {
name: string;
price: number;
stock: {
[size: string]: number;
};
}
In this example the name
and price
fields maintain their strict types, separate from the dynamically typed stock
object.
const tShirt: Product = {
name: 'T-Shirt',
price: 20,
stock: {
'S': 10,
'M': 15,
'L': 5,
},
};
// Accessing the 'M' stock value using nested object with bracket notation
console.log(product1.stock['M']); // Output: 15
This pattern is scalable and can be extended to include other dynamic properties if needed, by adding more nested objects or arrays with their specific types.
Example 3: Creating a Custom Utility Type
Index signatures are essential in TypeScript for creating complex utility types because they allow for flexible and dynamic data structures while maintaining type safety.
Suppose you have a type representing a user with several properties, and you want to create a new type where all of these properties are optional.
There is built-in utility type Partial
that does exactly that. Earlier, I wrote about TypeScript utility types.
However, for understanding of index signatures, let’s create a custom utility type Optional
that does the same thing.
type User = {
id: number;
name: string;
age: number;
email: string;
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
type OptionalUser = Optional<User>;
// OptionalUser is equivalent to:
// type OptionalUser = {
// id?: number | undefined;
// name?: string | undefined;
// age?: number | undefined;
// email?: string | undefined;
// }
In the above example, we created a custom utility type Optional
that takes a type T
and returns a new type where all properties of T
are optional.
Here we use index signatures to iterate over the keys of T
and make each property optional by adding a ?
after the property name.
This allows us to create a new type OptionalUser
that has all the properties of User
but with each property being optional.
Example 4: API response with dynamic keys
When working with APIs, you often receive data with a fixed set of properties and a dynamic set of properties. Index signatures are useful for defining types for such data.
Suppose you have an API that returns a response with a fixed set of properties (status, message) and a dynamic set of properties (data for different resources).
You can use an index signature to define a type for this response that allows for a fixed set of properties and a dynamic set of properties.
type ApiResponse = {
status: string;
message: string;
[resource: string]: any;
};
const response: ApiResponse = {
status: "success",
message: "Data fetched successfully",
users: [{ id: 1, name: "John" }, { id: 2, name: "Doe" }],
products: [{ id: 1, name: "Laptop", price: 1000 }],
// You can add more resources without changing the type
};
In this example, the ApiResponse
type has a fixed set of properties (status, message) and an index signature [resource: string]
that allows for any number of dynamic properties with any value type.
Conclusion
Index signatures are a powerful feature in TypeScript that allows you to define types for objects with unknown structures. They are especially useful when you want to create dictionary-like data structures or when you want to define complex utility types.
I hope this article has given you a good understanding of how to use index signatures in TypeScript and how they can be used to create complex and flexible types. If you have any questions or feedback, feel free to leave a comment below.
Check out my other TypeScript articles:
- Making React Components More Flexible with TypeScript Generics: 3 Examples
- 5 Resources Each TypeScript Developer Should Know About
- TypeScript Enums: 5 Real-World Use Cases
This article was originally posted on Medium.
Posted on March 13, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 27, 2024