Reputation: 1552
I was reading indexable types and "dictionary" pattern confused me:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
interface StringArray {
[index: number]: string;
}
What is the difference between the two?
How do you even initialize variable with the type of NumberOrStringDictionary
? And why does NumberOrStringDictionary
's fields (length
and name
) depend on index signatured field?
How is NumberOrStringDictionary
different from this:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
Upvotes: 8
Views: 10033
Reputation: 328097
This type:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number;
name: string;
}
implies that a value of type NumberOrStringDictionary
must contain a property named length
of type number
a property named name
of type string
. Additionally, a value of that type may contain any string
-named properties, as long as the values of such properties are assignable to string | number
.
A few points to clarify:
Index signatures may not conflict with any other keys. For example, the length
property is of type number
, so it is compatible with the index signature (number
is assignable to number | string
). And the name
property is also compatible. If you tried to add a property of some incompatible type, you'd get an error:
interface Oops extends NumberOrStringDictionary {
acceptable: boolean; // error!
//~~~~~~~~ <-- boolean not assignable to string | number
}
If I have a key of type string and I read the property at that key from a value of type NumberOrStringDictionary
, what is the type of that property? It is number | string
. Now imagine what would happen if you were allowed to add an incompatible property (like acceptable: boolean
). In that case it would no longer be true. If the key I choose happens to be "acceptable"
, then a boolean
comes out which is not number | string
, so something is wrong. To be safe, you'd have to expect number | string | boolean
.
You can think of the non-index signature properties as being special cases of the index signature, where you know the property key definitely exists and that the value is of some possibly more specific type. The known properties should be thought of as particular/special cases of the indexer. "All properties of this object are of type string | number
. In particular, the length
property of this object is of type number
, and the name
property of this object is of type string
". It's like saying "My pets are all dogs. Fido here is a poodle." Incompatibility would look like "My pets are all dogs. Fido here is a parakeet". You can say that, but it's not consistent. You might be thinking of index signatures as a "default" or "everything else" or "rest" case, ("My pets are all dogs except for this parakeet") but that's not what it means in TypeScript. There is an open issue, microsoft/TypeScript#17867 asking for such a construct in the language, but it is not yet there (there are workarounds).
An index-signature does not generally mean that all string
-keyed properties will appear. You are allowed to put as many or as few such properties as you'd like. It is therefore possible that when you inspect a property of type NumberOrStringDictionary
, it will be missing. When you read such a missing property, you will get a value of type undefined
, not of string | number
, but the compiler will pretend that it is string | number
. TypeScript 4.1 introduced a compiler flag to treat undefined
as a possible result of any index signature property read, but it is not on by default.
Perhaps it would be helpful to go through some examples and see what is accepted and what is rejected by the compiler. Here's a valid assignment:
const valid: NumberOrStringDictionary = {
length: 1,
name: "bob",
someOtherKey: 123,
someOtherKey2: "hey"
};
The requisite length
and name
properties are in there, and the additional properties match the index signature. This is also valid:
const alsoValid: NumberOrStringDictionary = {
length: 1,
name: "bob",
};
because you don't have to add any such index-signature properties. Now for the mistakes:
const invalid1: NumberOrStringDictionary = { // error!
//~~~~~~~~ <- property "name" is missing
length: 1,
someOtherKey: 123,
someOtherKey2: "key"
};
You can't leave out a required property. Also:
const invalid2: NumberOrStringDictionary = {
length: "fred", // error!
//~~~~ <-- string is not assignable to number
name: "bob",
someOtherKey: 123,
someOtherKey2: "key"
};
You can't put the wrong type of the required property. And finally:
const invalid3: NumberOrStringDictionary = {
length: 1,
name: "bob",
someOtherKey: 123,
someOtherKey2: true // error!
//~~~~~~~~~~~ <-- boolean is not string | number
};
You can't add a property that conflicts with the index signature.
Upvotes: 8
Reputation: 25966
Objects in JS can be thought of as a dictionary. They can take arbitrary keys, which map to corresponding values. This object - dictionary duality is also visible in syntax:
const o = {key: 1};
console.log(o['key']);
console.log(o.key)
Properties can be added and removed in runtime.
In general, most objects have a constant, known set of properties, and you model them as regular interfaces in TypeScript.
You need to use an indexed type if you want to model objects which follow JS rules
Note that there are 2 kinds of indices
In your example:
interface StringArray {
[index: number]: string; // number index
}
interface NumberOrStringDictionary {
[index: string]: number | string; // string index
length: number;
name: string;
}
You define the variables of these interfaces as follows:
const myArray: StringArray = ["Bob", "Fred"];
console.log(myArray[0])
const d: NumberOrStringDictionary = {
a: 1,
b: 2
length: 2,
name: 'myDict'
}
// You can add and remove properties in runtime
d.c = 1;
console.log(d['c'])
delete d.b;
console.log(d['c'])
Indexed types allow you to specify the types of values of the properties.
const d1: NumberOrStringDictionary = {
a: true, // error: Type 'boolean' is not assignable to type 'string | number'.
b: {}, // error: Type '{}' is not assignable to type 'string | number'.
length: 1,
name: 'myDict'
}
As described in the link from original post, if you add additional properties to a string-indexed type, the types of these properties must be compatible with the type specified for the index.
Upvotes: 2