Index Signatures in TypeScript
The original link: dmitripavlutin.com/typescript-…
Here, you use two objects to describe the salaries of two software developers:
const salary1 = {
baseSalary: 100 _000.yearlyBonus: 20 _000
};
const salary2 = {
contractSalary: 110 _000
};
Copy the code
You want to implement a function that returns the total salary based on the salary object:
function totalSalary(salaryObject: ???) {
let total = 0;
for (const name in salaryObject) {
total += salaryObject[name];
}
return total;
}
totalSalary(salary1); / / = > 120 _000
totalSalary(salary2); / / = > 110 _000
Copy the code
How would you annotate the salaryObject
parameter of the totalSalary()
function to accept objects with string keys and number values?
How would you annotate the salaryObject argument to the totalSalary() function to accept objects with string keys and values?
The answer is to use Index Signature!
Let’s find what are TypeScript index signatures and when they’re needed.
Let’s learn what TypeScript index signatures are and when to use them.
- 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.
The idea of using index Signature comes when you only know key and value types but need to enter objects of unknown structure.
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.
The index Signature fits the salary argument example above: this function accepts salary objects of different structure-but only numeric values.
Let’s annotate the salaryObject
parameter with an index signature:
Let’s annotate the salaryObject parameter with index Signature:
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
Copy the code
{ [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.
{[key: string]: number} is the index signature, which tells TypeScript that the salaryObject must be a string key and number value.
Now the totalSalary()
accepts as arguments both salary1
and salary2
objects, since they are objects with number values.
Now totalSalary() takes salary1 and salary2 objects as arguments because they are numeric only objects.
However, the function would not accept an object that has, for example, strings as values:
However, this function does not accept objects with values such as strings:
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'.
Copy the code
- Syntax for Index signature
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 }
.
The syntax for index Signature is very simple and looks similar to the syntax for attributes, with one difference. You simply write the type of the key in square brackets instead of the property name: {[key: KeyType]: ValueType}.
Here are a few examples of index signatures.
Here are a few examples of index signatures.
The string
type is the key and value:
Here the key and value are strings:
interface StringByString {
[key: string] :string;
}
const heroesInBooks: StringByString = {
'Gunslinger': 'The Dark Tower'.'Jack Torrance': 'The Shining'
};
Copy the code
The string
type is the key, the value can be a string
, number
, or boolean
:
The string type is the key, where the value can be string, number, or Boolean:
interface Options {
[key: string] :string | number | boolean;
timeout: number;
}
const options: Options = {
timeout: 1000.timeoutMessage: 'The request timed out! '.isFileUpload: false
};
Copy the code
Options
interface also has a field timeout
, which works fine near the index signature.
The Options interface also has a field timeout, which works fine around the Index signature.
The key of the index signature can only be a string
, number
, or symbol
. Other types are not allowed:
The index signature key can only be a string, number, or symbol. Other types are not allowed:
interface OopsDictionary {
[key: boolean] :string;
An index signature parameter type must be 'string'.'number'.'symbol', or a template literal type.}Copy the code
- Precautions for Index Signature
The index signatures in TypeScript have a few caveats you should be aware of.
Index Signatures in TypeScript have a few things you should be aware of.
3.1 Non-existent attributes
What would happen if you try to access a non-existing property of an object whose index signature is { [key: string]: string }
?
What happens if you try to access the nonexistent 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:
As expected, TypeScript extrapolates the type of a value to string. But if you check this value at runtime — it’s undefined:
interface StringByString {
[key: string] :string;
}
const object: StringByString = {};
const value = object['nonExistingProp'];
value; // => undefined
const value: stringxxxxxxxxxx interface StringByString { [key: string] :string; }const object: StringByString = {}; const value = object['nonExistingProp']; value;// => undefined const value: stringinterface StringByString { [key: string]: string; } const object: StringByString = {}; const value = object['nonExistingProp']; value; // => undefined const value: string
Copy the code
value
variable is a string
type according to TypeScript, however, its runtime value is undefined
.
According to TypeScript syntax, the value variable should be of type string, but 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.
Index Signature just maps key types to value types, that’s all. If you make this mapping problematic, the runtime value type may deviate from the actual 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:
For more accurate input, mark the index value as string or undefined. In doing so, TypeScript realizes that the property you’re accessing may not exist:
interface StringByString {
[key: string] :string | undefined;
}
const object: StringByString = {};
const value = object['nonExistingProp'];
value; // => undefined
const value: string | undefined
Copy the code
3.2 String and numeric keys
Suppose you have a dictionary of numeric names:
interface NumbersNames {
[key: string] :string
}
const names: NumbersNames = {
'1': 'one'.'2': 'two'.'3': 'three'.// etc...
};
Copy the code
Accessing a value by a string key works as expected:
A value is accessed by a string key of the expected type:
const value1 = names['1']; const value1: string
Copy the code
Would it be an error if you try to access a value by a number 1
?
If you try to access a value by the number 1, will the value fail?
const value2 = names[1]; const value2: string
Copy the code
Nope, all good!
No, everything’s fine!
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.
When used as a key in a property accessor, JavaScript implicitly casts numbers to strings (names[1] is the same as names[‘1’]). TypeScript does this as well.
You can think that [key: string]
is the same as [key: string | number]
.
You can think [key: string] and [key: string | number] is the same.
- Index signature vs Record<Keys, Type>
TypeScript has a utility type Record<Keys, Type>
to annotate records, similar to the index signature.
TypeScript has a utility Type Record
to annotate records, similar to index signature.
const object1: Record<string.string> = { prop: 'Value' }; // OK
const object2: { [key: string] :string } = { prop: 'Value' }; // OK
Copy the code
The big question is… when to use a Record
and when an index signature? At first sight, they look quite similar!
The biggest problem is… When to use Record
and when to use index Signature? At first glance, they do look alike!
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:
As you saw earlier, index Signature only accepts string,number, or Symbol as key types. If you try to use a union of string literal types as a key in an index signature, you get an error:
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.
}
Copy the code
This behavior suggests that the index signature is meant to be generic in regards to keys.
This behavior indicates that the index signature is generic for keys.
But you can use a union of string literals to describe the keys in a Record<Keys, Type>
:
But you can use a union of string literals to describe Keys in Record
:
type SpecificSalary = Record<'yearlySalary'|'yearlyBonus'.number>
const salary1: SpecificSalary = {
'yearlySalary': 120 _000.'yearlyBonus': 10 _000
}; // OK
Copy the code
The Record<Keys, Type>
is meant to be specific in regards to keys.
Record
may only treat Keys specifically.
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.
I recommend using index Signature to annotate generic objects, such as keys of string type. But when you know in advance key, should use Record < Keys, Type > annotations for specific objects, such as the combination of string Type ‘prop1’ | ‘prop2 for key.
- 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.
If you don’t know the structure of the object you’re going to use, but know the possible key and value types, 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.
Index signature consists of the index name and its type enclosed in square brackets, followed by a colon and ValueType: {[indexName: KeyType]: ValueType}. KeyType can be string, Number, or symbol, and ValueType can be any type.