index-types-and-index-signatures
Index Types and Index Signatures:
Let's take a look at each of these points with an accompanying TypeScript example:
- Index Types:
Index types are powerful because they allow you to define the structure of objects that are used like dictionaries. This is handy when you want to make sure that all the keys in the object have values of the same type.
interface IndexTypeExample {
[key: string]: number;
}
const example: IndexTypeExample = {
age: 30,
score: 100
};
- Index Signatures:
Index signatures define what types you can expect to retrieve from an object when you access it with a key. This makes sure that no matter what key you use, as long as it's the right type, you will always get a value of the expected type back.
interface StringToNumberMap {
[key: string]: number;
}
const map: StringToNumberMap = {
apples: 5,
bananas: 10
};
- Dynamic Property Names:
When you aren't sure of the exact property names but you know the structure of the values, index signatures are extremely useful. They let you capture the essence of the object's structure without needing to specify every property name.
interface AnyProps {
[prop: string]: boolean;
}
const features: AnyProps = {
isEnabled: true,
isVisible: false
};
- Keys as
string
ornumber
:
Since JavaScript object keys are always strings, even when you put numbers there, TypeScript allows you to use string
or number
as types for the keys in index signatures.
interface NumberDictionary {
[index: number]: string;
}
const dictionary: NumberDictionary = {
10: "ten",
20: "twenty"
};
- Autocompletion and Type-checking:
Index types enhance autocompletion for object keys in code editors and enforce types for values, making it much harder to introduce a bug by accidentally assigning the wrong type to a property.
interface Config {
[setting: string]: string | number;
}
const config: Config = {
theme: "dark",
maxCount: 10
};
- Wide Range of Property Names:
When you have a function that could be dealing with various objects with many different property names, index types can define an interface for this function to work with any of them as long as they follow the same value pattern.
function getProperty<T, K extends string>(obj: T, key: K) {
return obj[key];
}
const obj = { a: 1, b: 2, c: 3 };
const value = getProperty(obj, "a"); // value is inferred as number
- Enforcing Property Types:
With an index signature, you can enforce that all properties on an object are of the same type, which is very useful for ensuring consistency throughout an object.
interface HomogeneousObject {
[key: string]: string;
}
const goodObject: HomogeneousObject = {
prop1: "value1",
prop2: "value2"
};
// This would cause an error:
// const badObject: HomogeneousObject = {
// prop1: "value1",
// prop2: 100 // Error: Type 'number' is not assignable to type 'string'
// };
These examples illustrate how you can use index types and signatures to define objects that behave like dictionaries with dynamic keys but still have strong typing.
Let's go through these points with examples to understand how they can be applied in TypeScript:
- Combining Index Signatures with Specific Properties:
You can define an object that has both explicit properties for known elements and an index signature for additional properties that may be added dynamically.
interface PersonInfo {
name: string;
age: number;
[propName: string]: string | number;
}
const person: PersonInfo = {
name: "Alice",
age: 25,
email: "alice@example.com" // Additional property
};
- Precision with Index Signatures:
While index signatures are flexible, they can sometimes make your types less precise, potentially hiding errors. It's often better to avoid an index signature if you know the exact shape of your object.
interface AnyValues {
[prop: string]: any; // Avoid this where possible
}
- Index Types and Mapped Types:
Index types are often used in combination with mapped types to transform properties from one type to another type.
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface MutablePerson {
name: string;
age: number;
}
const readonlyPerson: Readonly<MutablePerson> = {
name: "Alice",
age: 25
};
// readonlyPerson.name = "Bob"; // Error: Cannot assign to 'name' because it is a read-only property.
- Symbol Index Signature:
TypeScript does not support symbols as index signature parameter types because symbols are not compatible with ES5 indexing.
- Numeric Index Types:
If an object has a numeric index signature, TypeScript ensures that all numeric properties conform to the type specified by that numeric index.
interface NumericIndexType {
[index: number]: string;
// 1: "one", // Correct
// 2: 2 // Error: Type 'number' is not assignable to type 'string'.
}
- Readonly Index Signatures:
Readonly index signatures are useful for when you want to create objects that cannot be changed after their creation.
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
// myArray[2] = "Charlie"; // Error: Index signature in type 'ReadonlyStringArray' only permits reading.
- Conforming to Index Signature:
All properties in a type with an index signature must conform to the index signature's value type.
interface ConformingProperties {
[key: string]: number;
x: number;
// y: string; // Error: Type 'string' is not assignable to type 'number'.
}
- Advanced Index Types:
You can use index types in more complex ways by intersecting them with other types or using them within generics.
type IntersectionType = { [key: string]: number } & { fixedProp: string };
type GenericConstraint<K extends string> = { [P in K]: number };
These examples show how you can use index types and their associated features to create flexible and strongly-typed objects in TypeScript. Understanding index types and index signatures is key for writing robust and flexible TypeScript code, especially when dealing with dynamic object keys.