I've been writing TypeScript for a while and am confused about what an index signature means.
For example, this code is legal:
function fn(obj: { [x: string]: number }) {
let n: number = obj.something;
}
But this code, which does basically the same thing, isn't:
function fn(obj: { [x: string]: number }) {
let p: { something: number } = obj;
}
Is this a bug? What's the intended meaning of this?
Index signature is used to represent the type of object/dictionary when the values of the object are of consistent types. Syntax: { [key: KeyType] : ValueType } Assume that we have a theme object which allows us to configure the color properties that can be used across the application.
The error "Property is incompatible with index signature" occurs when a property is not compatible with the specified type of the index signature. To solve the error, change the type of the property or use a union to update the type in the index signature.
Use an index signature to define a key-value pair in TypeScript, e.g. const employee: { [key: string]: string | number } = {} .
A mapped object type operates on a set of singleton types and produces a new object type where each of those singletons is turned into a property name. For example, this: type Foo = { [K in "hello" | "world"]: string }; would be equivalent to type Foo = { "hello": string; "world": string; };
TypeScript has a utility type Record<Keys, Type> to annotate records, similar to the index signature. 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.
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.
In your case, your keys can only be of type string. And your values can be whatever. The reason behind this is that, in typescript, you can define an object to have certain property : This create an type of object what MUST have both property to be valid. Because of that, there is not intuitive way to define key type.
Using Map/Set types over index signatures allows us to avoid quirks such as accessing Object.prototype values when accessing the keys of an object built using an index signature. What to do when you know the keys in your type but are not sure how many there will be?
You are right to be confused. Index signatures mean a few things, and they mean slightly different things depending where and how you ask.
First, index signatures imply that all declared properties in the type must have a compatible type.
interface NotLegal {
// Error, 'string' isn't assignable to 'number'
x: string;
[key: string]: number;
}
This is a definitional aspect of index signatures -- that they describe an object with different property keys, but a consistent type across all those keys. This rule prevents an incorrect type from being observed when a property is accessed through an indirection:
function fn(obj: NotLegal) {
// 'n' would have a 'string' value
const n: number = obj[String.fromCharCode(120)];
}
Second, index signatures allow writing to any index with a compatible type.
interface NameMap {
[name: string]: number;
}
function setAge(ageLookup: NameMap, name: string, age: number) {
ageLookup[name] = age;
}
This is a key use case for index signatures: You have some set of keys and you want to store a value associated with the key.
Third, index signatures imply the existence of any property you specifically ask for:
interface NameMap {
[name: string]: number;
}
function getMyAge(ageLookup: NameMap) {
// Inferred return type is 'number'
return ageLookup["RyanC"];
}
Because x["p"]
and x.p
have identical behavior in JavaScript, TypeScript treats them equivalently:
// Equivalent
function getMyAge(ageLookup: NameMap) {
return ageLookup.RyanC;
}
This is consistent with how TypeScript views arrays, which is that array access is assumed to be in-bounds. It's also ergonomic for index signatures because, very commonly, you have a known set of keys available and don't need to do any additional checking:
interface NameMap {
[name: string]: number;
}
function getAges(ageLookup: NameMap) {
const ages = [];
for (const k of Object.keys(ageLookup)) {
ages.push(ageLookup[k]);
}
return ages;
}
However, index signatures don't imply that any arbitrary, unspecific property will be present when it comes to relating a type with an index signature to a type with declared properties:
interface Point {
x: number;
y: number;
}
interface NameMap {
[name: string]: number;
}
const m: NameMap = {};
// Not OK, which is good, because p.x is undefined
const p: Point = m;
This sort of assignment is very unlikely to be correct in practice!
This is a distinguishing feature between { [k: string]: any }
and any
itself - you can read and write properties of any kind on an object with an index signature, but it can't be used in place of any type whatsoever like any
can.
Each of these behaviors is individually very justifiable, but taken as a whole, some inconsistencies are observable.
For example, these two functions are identical in terms of their runtime behavior, but TypeScript only considers one of them to have been called incorrectly:
interface Point {
x: number;
y: number;
}
interface NameMap {
[name: string]: number;
}
function A(x: NameMap) {
console.log(x.y);
}
function B(x: Point) {
console.log(x.y);
}
const m: NameMap = { };
A(m); // OK
B(m); // Error
Overall, when you write an index signature on a type, you're saying:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With