TypeScript Index Signatures: 4 Examples Type-Safe Dynamic Objects

alexefimenko

Alex

Posted on March 13, 2024

TypeScript Index Signatures: 4 Examples Type-Safe Dynamic Objects

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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;
  };
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
// }
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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:

This article was originally posted on Medium.

💖 💪 🙅 🚩
alexefimenko
Alex

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