Review of Index Signatures
First, recall index signatures. They're often used to loosely type out an object.
type Fruit = {
name: string;
color: string;
mass: number;
};
type FruitBasket = {
[k: string]: Fruit;
};
// Or used with Generics
type Dict<T> = {
[k: string]: T;
// [k: string]: T | undefined; // SAFER
};
const fruitCatalog: Dict<Fruit> = {};
fruitCatalog.pineapple; // <- allowed unless we use `T | undefined`
Index signature parameter types (k
) can be an arbitrary (any) string
or number
.
You can NOT specify a more narrow subset of strings or numbers when using an index signature.
type MyFruits = { [key: 'apple' | 'cherry']: Fruit }; // <- ERROR
// Error: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
They are not allowed to be a literal type or a type parameter (generic).
For that we need Mapped Types.
Mapped Types
Mapped types allow us to specify a specific subset of keys.
This then allows us to transform types by taking one type and making it another type in a very structured and deliberate way.
Think of mapped types as being analogous to the map
function used with arrays (we'll see that in use later).
Here's how a very basic mapped type looks different than an index signature:
// Index Signature
{ [keyNameDoesNotMatter: string]: ... }
// Mapped Type
{ [FruitKey in "apple" | "cherry"]: ... }
In the larger context:
type Fruit = {
name: string;
color: string;
mass: number;
};
// mapped type
type MyRecord = { [FruitKey in 'apple' | 'cherry']: Fruit };
function printFruitCatalog(fruitCatalog: MyRecord) {
fruitCatalog.cherry;
fruitCatalog.apple;
// (property) apple: Fruit
fruitCatalog.pineapple; // <- ERROR
// Error: Property 'pineapple' does not exist on type 'MyRecord'.
}
NOTE the
in
keyword in the mapped type.
'apple' | 'cherry'
is the part we're looping over.
The key name (in this case FruitKey
) will be important and come into use later with more complex usage of mapped types.
Record
If we make our type a bit more generalized (with some type params
instead of hardcoding Fruit
and "apple" | "cherry"
as shown below) we'll arrive at a built-in utility type that comes with TypeScript.
- type MyRecord = { [FruitKey in "apple" | "cherry"]: Fruit }
+ type MyRecord<KeyType, ValueType> = { [Key in KeyType]: ValueType }
Or more specifically:
type MyRecord<KeyType extends string, ValueType> = {
[Key in KeyType]: ValueType;
};
Here's the built-in TypeScript type, which matches this pretty much exactly:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
You may notice the keyof any
difference. It just means string | number | symbol
.
From the Intermediate TypeScript↗ course on FEM↗ taught by Mike North↗.