Mastering TypeScript Index Signature

Mastering TypeScript Index Signature

Sunny Sun Lv4

** The post is updated on 28-07-2024

TypeScript provides powerful tools for type-checking and defining the shape of objects. One such feature is the “index signature,” which allows you to describe the types of properties and their values within an object.

Index signatures can define a dynamic data structure when the properties of an object aren’t known beforehand, but the types of properties are known. They allow for dynamic property access and are particularly useful when working with objects with a variable set of keys.

This post will delve into the index signature, how to use it, and when to use it in TypeScript.

Index signature overview

An index signature is defined using square brackets [] and the type for keys, followed by a colon and the type for corresponding values. It enables TypeScript to understand and enforce the expected structure of the object.

1
2
3
4
5
6
7
8
9
10
11
12
13
interface MyStats {
[key: string]: number;
}
const scores: MyStats = {
total: 50,
average:80
}
// index siganture enforce the type constraint
// here, the value must be a number
const scores2: MyStats = {
total: "50", //Type 'string' is not assignable to type 'number'.(2322)
average:80
}

In this example, MyStats can have any string keys, and the values associated with those keys must be of type number.

The syntax for index signatures involves using the [] notation within the interface or type declaration. The below example shows the same index signature for interface and type.

1
2
3
4
5
6
7
interface Car {
[key: string]: boolean;
}

type CarType = {
[key: string]: boolean;
}

Note that index signatures can use different key types, such as string, number, symbol or literal typeand the associated value type can be any valid TypeScript type.

Index signature with explicit members

In TypeScript, we can mix an index signature with explicit member declarations. It is helpful for cases requiring a combination of known and dynamic properties.

1
2
3
4
interface CarConfiguration {
[feature: string]: number;
price: number;
}

When we mix the index signature with explicit members, all explicit members need to conform to the index signature types.

1
2
3
4
5
6
7
8
9
10
11
12
13
// invalid case
interface CarConfiguration {
[feature: string]: number;
price: number;
model: string; // Error: Property 'model' of type 'string' is not assignable to 'string' index type 'number'
}

// valid
interface CarConfiguration {
[feature: string]: number | string;
price: number;
model: string;
}

Readonly index signature

Index signature supports readonly modifier. By applying the readonly modifier, the properties in the object will be immutable.

1
2
3
4
5
6
interface Car {
readonly [key: string]: boolean;
}

const toyota: Car = {hybrid: true, luxury: false};
toyota.hybrid = false; //Error: Index signature in type 'Car' only permits reading.(2542)

In the above example, an error occurs when trying to modify the ‘hybrid’ property because the interface allows only reading, not writing.

Practical Use Cases

Maps and Dictionaries

Index signatures are ideal for creating maps or dictionaries where the keys are dynamic but the values have a consistent shape.

Let’s see a real-world example of how index signatures can be used. Imagine we’re developing a web application with various features. Each feature includes its own set of settings. We are also able to enable or disable these features.

1
2
3
4
5
6
interface FeatureConfig {
[feature: string]: {
enabled: boolean;
settings: Record<string, boolean>;
}
}

In this example, we define an interface named FeatureConfig. It uses an index signature to allow dynamic property names of type string associated with anenabled boolean property and a settings object. It is handy for representing configurations with dynamic feature names and associated settings. For example, we can apply the interface to the following object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const features: FeatureConfig = {
profile: {
enabled: true,
settings: {
showPhoto: true,
allowEdit: false,
},
},
notification: {
enabled: false,
settings: {
richText: true,
batchMode: true
},
}
};

In the features object, the feature names can vary, but the structure for each feature remains consistent. Each feature is expected to have an enabled boolean and a settings object.

To improve the type safety, can we apply a union-type constraint to the feature name in the above interface?

If the set of features in our application is known, we can define the union of string literals namedFeatureType.

1
type FeatureType = 'profile' | 'notification' | 'reporting';  

The key of the index signature does not support the union type, but we can work around it using a mapped type.

1
2
3
4
5
6
type FeatureConfig2 = {
[feature in FeatureType]: {
enabled: boolean;
settings: Record<string, boolean>;
}
}

[feature in FeatureType]is a mapped type that iterates over each string literal in the union type FeatureType (which includes ‘profile’, ‘notification’, and ‘reporting’), and it uses each value as the resulting type’s property name.

Here’s an example of how we might use it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const allFeatures: FeatureConfig2 = {
profile: {
enabled: true,
settings: {
showPhoto: true,
allowEdit: false,
},
},
notification: {
enabled: false,
settings: {
richText: true,
batchMode: true
},
},
reporting: {
enabled: true,
settings: {
template: false,
advanceExport: true
},
},
};

Note that we need to include all features defined in FeatureType to the object to match the type expectations.

If we want to allow a subset of the features as the key, we need to modify the index signature type with an “?” as an optional flag. Then, we could use the FeatureConfig2 type for an object that only contains a subset of features.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type FeatureType = 'profile' | 'notification' | 'reporting';

type FeatureConfig2 = {
[feature in FeatureType]?: {
enabled: boolean;
settings: Record<string, boolean>;
}
}

const subsetFeatures: FeatureConfig2 = {
profile: {
enabled: true,
settings: {
showPhoto: true,
allowEdit: false,
},
}
};

Index signature vs Record type

A common question about index signature is “Why not use record type instead?”.

Record type defines an object type where we know the specific set of properties and their corresponding types. For example below, the UserRole type restricts the keys to “admin”, “editor” or “viewer”.

1
2
3
4
5
6
7
8
9
type UserRole = "admin" | "editor" | "viewer";

type UserPermissions = Record<UserRole, boolean>;

const permissions: UserPermissions = {
admin: true,
editor: false,
viewer: true,
};

For index signature, we don’t know the specific properties and only the general type of the property types. Thus, the use case of index signature is to model dynamic objects, and the record type can be used for known object types.

How to use index signatures effectively

Some commonly used scenarios include:

  • Configuration Objects: As the above example illustrates, index signatures excel in scenarios where configuration objects may have dynamic keys and associated values.
  • Data Transformation: Index signatures can be beneficial when dealing with data transformations or parsing. They allow for flexible handling of input data with varying structures.
  • Extensibility: In projects where extensibility is a priority, such as plugin architectures or modular systems, index signatures enable adding new components without modifying existing code.

Limitations

While powerful, index signatures should not be overused. Before implementing an index signature, consider whether a more explicit interface or type definition could better represent the data structure, especially when the keys have specific meanings.

Index signatures do not allow the definition of optional properties. If you need optional properties, you should define them explicitly in the type or interface.

Another consideration is to apply rigorously test scenarios involving index signatures. This includes testing various key-value combinations to ensure that the dynamic nature of the structure does not introduce unforeseen issues.

Summary

An index signature is ideal when we don’t know the exact structure of an object, but we do know the types of the keys and values.
By avoiding common mistakes and following best practices, we can use index signatures to make TypeScript code more flexible and resilient.

  • Title: Mastering TypeScript Index Signature
  • Author: Sunny Sun
  • Created at : 2024-02-01 00:00:00
  • Updated at : 2024-07-28 14:09:08
  • Link: http://coffeethinkcode.com/2024/02/01/mastering-typescript-index-signature/
  • License: This work is licensed under CC BY-NC-SA 4.0.