You have 2 objects that describe the salary of 2 software developers:
ts
constsalary1 = {baseSalary : 100_000,yearlyBonus : 20_000};constsalary2 = {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_000totalSalary(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.
Table of Contents
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
functiontotalSalary (salaryObject : { [key : string]: number }) {lettotal = 0;for (constname insalaryObject ) {total +=salaryObject [name ];}returntotal ;}totalSalary (salary1 ); // => 120_000totalSalary (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
constsalary3 = {baseSalary : '100 thousands'};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'.totalSalary (); salary3
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
interfaceStringByString {[key : string]: string;}constheroesInBooks :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
interfaceOptions {[key : string]: string | number | boolean;timeout : number;}constoptions :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
interfaceOopsDictionary {[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.: boolean]: string; key }
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
interfaceStringByString {[key : string]: string;}constobject :StringByString = {};constvalue =object ['nonExistingProp'];value ; // => undefined
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
interfaceStringByString {[key : string]: string | undefined;}constobject :StringByString = {};constvalue =object ['nonExistingProp'];value ; // => 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
interfaceNumbersNames {[key : string]: string}constnames :NumbersNames = {'1': 'one','2': 'two','3': 'three',// etc...};
Accessing a value by a string key works as expected:
ts
constvalue1 =names ['1'];
Would it be an error if you try to access a value by a number 1
?
ts
constvalue2 =names [1];
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
constobject1 :Record <string, string> = {prop : 'Value' }; // OKconstobject2 : { [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
interfaceSalary {[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.: 'yearlySalary' | 'yearlyBonus']: number key }
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
typeSpecificSalary =Record <'yearlySalary'|'yearlyBonus', number>constsalary1 :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.