Learn how to use indexable types in TypeScript to type arrays and objects accessed via indices or keys. Explore index signatures, dynamic keys, and practical examples.
In TypeScript, indexable types allow us to define the shape of objects and arrays that are accessed using indices or keys. This feature is particularly useful when working with collections of data where the keys or indices are not known in advance. In this section, we’ll explore how to use index signatures in interfaces, provide examples of typing objects with dynamic keys and arrays, discuss the limitations and rules of indexable types, and highlight scenarios where they are particularly useful. We’ll also include warnings about potential type safety issues with improper use.
Index signatures are a way to describe the types of values that can be accessed using a string or number index. They are declared within interfaces and allow us to define properties whose names are not known at compile time.
To declare an index signature in an interface, we use the following syntax:
interface MyIndexableType {
[index: string]: number;
}
In this example, MyIndexableType
is an interface with an index signature that specifies any property accessed with a string
key will have a number
value.
Consider a scenario where we have an object representing a collection of scores for different players. The player names are not known in advance, so we use an index signature to type this object:
interface PlayerScores {
[playerName: string]: number;
}
const scores: PlayerScores = {
Alice: 10,
Bob: 15,
Charlie: 20
};
console.log(scores["Alice"]); // Output: 10
In this example, the PlayerScores
interface allows us to access player scores using their names as keys.
Index signatures can also be used to type arrays. In TypeScript, arrays are objects with numeric indices, so we can use a number index signature to define the type of elements in an array.
Let’s create an interface for an array of strings:
interface StringArray {
[index: number]: string;
}
const fruits: StringArray = ["Apple", "Banana", "Cherry"];
console.log(fruits[1]); // Output: Banana
In this example, the StringArray
interface ensures that every element in the fruits
array is a string.
While indexable types are powerful, there are some limitations and rules to keep in mind:
Consistency in Value Types: All properties accessed via an index signature must have the same type. For example, if you declare [index: string]: number
, all values must be numbers.
No Mixed Index Signatures: You cannot have both string and number index signatures in the same interface. This is because JavaScript automatically converts number indices to strings when accessing object properties.
No Overlapping Properties: If an interface has both named properties and an index signature, the named properties must be compatible with the index signature type.
interface MixedType {
[index: string]: number;
length: number; // This is allowed
// name: string; // Error: Property 'name' of type 'string' is not assignable to string index type 'number'.
}
In this example, the length
property is compatible with the index signature, but a name
property would cause an error.
Indexable types are particularly useful in scenarios where:
Dynamic Data Structures: You are working with data structures where the keys or indices are not known at compile time, such as dictionaries or maps.
Collections of Similar Items: You need to enforce a consistent type for all items in a collection, such as arrays or lists.
Flexible APIs: You are designing APIs that need to accommodate a wide range of input types without sacrificing type safety.
While indexable types provide flexibility, improper use can lead to type safety issues. Here are some warnings to consider:
Type Mismatch: Ensure that the values assigned to properties accessed via an index signature match the expected type. Type mismatches can lead to runtime errors.
Unintentional Property Access: Be cautious when accessing properties that may not exist. Use optional chaining or type guards to handle undefined values safely.
interface ProductPrices {
[productName: string]: number;
}
const prices: ProductPrices = {
apple: 1.5,
banana: 2.0
};
const orangePrice = prices["orange"] ?? 0; // Use nullish coalescing to provide a default value
console.log(orangePrice); // Output: 0
In this example, we use the nullish coalescing operator (??
) to provide a default value when accessing a property that may not exist.
Now that we’ve covered the basics of indexable types, try experimenting with the following exercises:
Create a Dictionary: Define an interface for a dictionary that maps country names to their capitals. Populate the dictionary with a few entries and access the capital of a specific country.
Type an Array of Numbers: Create an interface for an array of numbers and use it to define an array of your favorite numbers. Access and log a specific number from the array.
Handle Undefined Properties: Extend the PlayerScores
example to handle cases where a player’s score is not available. Use optional chaining or default values to prevent errors.
To help visualize the concept of indexable types, consider the following diagram:
graph TD; A[Interface with Index Signature] --> B[Dynamic Keys] A --> C[Arrays with Numeric Indices] B --> D[PlayerScores Example] C --> E[StringArray Example]
This diagram illustrates how an interface with an index signature can be used to type objects with dynamic keys and arrays with numeric indices.
For further reading on indexable types and related concepts, check out these resources:
To reinforce your understanding of indexable types, consider these questions: