TypeScript Index Signature Explained

TypeScript Index Signature Explained

Sunny Sun lol

TypeScript Index Signature Explained

Demystifying TypeScript Index Signatures for Enhanced Code Flexibility

Photo by [Maksym Kaharlytskyi](https://unsplash.com/@qwitka?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) on [Unsplash](https://unsplash.com/photos/file-cabinet-Q9y3LRuuxmg?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)Photo by Maksym Kaharlytskyi on Unsplash

Index signatures in TypeScript provide a way to 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.

What is the index signature?

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.

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.

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.

Mixing an 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.

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.
// 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.
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.

How to use index signature

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.
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.

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.
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.

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:
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.

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,
    },
  }
};

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.

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.

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.

By avoiding common mistakes and following best practices, we can use index signatures to make TypeScript code more flexible and resilient.

Happy programming!