Post cover

Index Signatures in TypeScript

Posted September 22, 2021

You have 2 objects that describe the salary of 2 software developers:

ts
const salary1 = {
baseSalary: 100_000,
yearlyBonus: 20_000
};
 
const salary2 = {
contractSalary: 110_000
};

You want to implement a function that returns the total remuneration based on the salary object:

ts
function totalSalary(salaryObject: ???) {
let total = 0;
for (const name in salaryObject) {
total += salaryObject[name];
}
return total;
}
totalSalary(salary1); // => 120_000
totalSalary(salary2); // => 110_000

How would you annotate the salaryObject parameter of the totalSalary() function to accept objects with string keys and number values?

The answer is to use an index signature!

Let's find what are TypeScript index signatures and when they're needed.

1. Why index signature

The idea of the index signatures is to type objects of unknown structure when you only know the key and value types.

An index signature fits the case of the salary parameter: the function should accept salary objects of different structures — only that values to be numbers.

Let's annotate the salaryObject parameter with an index signature:

ts
function totalSalary(salaryObject: { [key: string]: number }) {
let total = 0;
for (const name in salaryObject) {
total += salaryObject[name];
}
return total;
}
 
totalSalary(salary1); // => 120_000
totalSalary(salary2); // => 110_000

{ [key: string]: number } is the index signature, which tells TypeScript that salaryObject has to be an object with string type as key and number type as value.

Now the totalSalary() accepts as arguments both salary1 and salary2 objects, since they are objects with number values.

However, the function would not accept an object that has, for example, strings as values:

ts
const salary3 = {
baseSalary: '100 thousands'
};
 
totalSalary(salary3);
Argument of type '{ baseSalary: string; }' is not assignable to parameter of type '{ [key: string]: number; }'. Property 'baseSalary' is incompatible with index signature. Type 'string' is not assignable to type 'number'.2345Argument of type '{ baseSalary: string; }' is not assignable to parameter of type '{ [key: string]: number; }'. Property 'baseSalary' is incompatible with index signature. Type 'string' is not assignable to type 'number'.

2. Index signature syntax

The syntax of an index signature is pretty simple and looks similar to the syntax of a property, but with one difference. Instead of the property name, you simply write the type of the key inside the square brackets: { [key: KeyType]: ValueType }.

Here are a few examples of index signatures.

The string type is the key and value:

ts
interface StringByString {
[key: string]: string;
}
 
const heroesInBooks: StringByString = {
'Gunslinger': 'The Dark Tower',
'Jack Torrance': 'The Shining'
};

The string type is the key, the value can be a string, number, or boolean:

ts
interface Options {
[key: string]: string | number | boolean;
timeout: number;
}
 
const options: Options = {
timeout: 1000,
timeoutMessage: 'The request timed out!',
isFileUpload: false
};

Options interface also has a field timeout, which works fine near the index signature.

The key of the index signature can only be a string, number, or symbol. Other types are not allowed:

ts
interface OopsDictionary {
[key: boolean]: string;
An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.1268An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.
}

3. Index signature caveats

The index signatures in TypeScript have a few caveats you should be aware of.

3.1 Non-existing properties

What would happen if you try to access a non-existing property of an object whose index signature is { [key: string]: string }?

As expected, TypeScript infers the type of the value to string. But if you check the runtime value — it's undefined:

ts
interface StringByString {
[key: string]: string;
}
 
const object: StringByString = {};
 
const value = object['nonExistingProp'];
value; // => undefined
const value: string

value variable is a string type according to TypeScript, however, its runtime value is undefined.

The index signature simply maps a key type to a value type, and that's all. If you don't make that mapping correct, the value type can deviate from the actual runtime data type.

To make typing more accurate, mark the indexed value as string or undefined. Doing so, TypeScript becomes aware that the properties you access might not exist:

ts
interface StringByString {
[key: string]: string | undefined;
}
 
const object: StringByString = {};
 
const value = object['nonExistingProp'];
value; // => undefined
const value: string | undefined

`string | undefined` because the property can be missing

3.2 String and number key

Let's say that you have a dictionary of number names:

ts
interface NumbersNames {
[key: string]: string
}
 
const names: NumbersNames = {
'1': 'one',
'2': 'two',
'3': 'three',
// etc...
};

Accessing a value by a string key works as expected:

ts
const value1 = names['1'];
const value1: string

Would it be an error if you try to access a value by a number 1?

ts
const value2 = names[1];
const value2: string

Nope, all good!

JavaScript implicitly coerces numbers to strings when used as keys in property accessors (names[1] is the same as names['1']). TypeScript performs this coercion too.

You can think that [key: string] is the same as [key: string | number].

4. Index signature vs Record<Keys, Type>

TypeScript has a utility type Record<Keys, Type> to annotate records, similar to the index signature.

ts
const object1: Record<string, string> = { prop: 'Value' }; // OK
const object2: { [key: string]: string } = { prop: 'Value' }; // OK

The big question is... when to use a Record<Keys, Type> and when an index signature? At first sight, they look quite similar!

As you saw earlier, the index signature accepts only string, number or symbol as key type. If you try to use, for example, a union of string literal types as keys in an index signature, it would be an error:

ts
interface Salary {
[key: 'yearlySalary' | 'yearlyBonus']: number
An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.1337An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
}

This behavior suggests that the index signature is meant to be generic in regards to keys.

But you can use a union of string literals to describe the keys in a Record<Keys, Type>:

ts
type SpecificSalary = Record<'yearlySalary'|'yearlyBonus', number>
 
const salary1: SpecificSalary = {
'yearlySalary': 120_000,
'yearlyBonus': 10_000
}; // OK

Be specific: indicate the keys of the object

The Record<Keys, Type> is meant to be specific in regards to keys.

I recommend using the index signature to annotate generic objects, e.g. keys are string type. But use Record<Keys, Type> to annotate specific objects when you know the keys in advance, e.g. a union of string literals 'prop1' | 'prop2' is used for keys.

5. Conclusion

If you don't know the object structure you're going to work with, but you know the possible key and value types, then the index signature is what you need.

The index signature consists of the index name and its type in square brackets, followed by a colon and the value type: { [indexName: KeyType]: ValueType }. KeyType can be a string, number, or symbol, while ValueType can be any type.

Like the post? Please share!

Quality posts into your inbox

I regularly publish posts containing:

  • Important JavaScript concepts explained in simple words
  • Overview of new JavaScript features
  • How to use TypeScript and typing
  • Software design and good coding practices

Subscribe to my newsletter to get them right into your inbox.

Join 4774 other subscribers.
Dmitri Pavlutin

About Dmitri Pavlutin

Tech writer and coach. My daily routine consists of (but not limited to) drinking coffee, coding, writing, coaching, overcoming boredom 😉.
Email addressTwitter profileFacebook pageLinkedIn profile