Indexed Access Types
In TypeScript you can reuse the type of a property of another type.
interface User {
id: number;
name: string;
address: {
street: string;
city: string;
country: string;
};
}
In the code above we can reuse the types of the User interface's id
and address
properties.
Let's say, I need to create a function for updating the address of a user:
function updateAddress(
id: User['id'],
address: User['address']
) {}
Instead of using a number to describe the id
parameter I described it using the User['id']
type which refers to the type of the id
property from the User interface. This type is called index access type or lookup type. And for the address
parameter I used the type of the address
property.
We can access the types of nested properties as well:
type City = User['address']['city']; // string
And we can get the types of multiple properties at once:
type IdOrName = User['id' | 'name']; // string | number
Of course, I could split the User interface into multiple types and reuse those types instead of using the lookup types:
type UserId = number;
interface UserAddress {
street: string;
city: string;
country: string;
}
interface User {
id: UserId;
name: string;
address: UserAddress;
}
function updateAddress(id: UserId, address: UserAddress) {}
Splitting a large type into multiple types looks fine, as long as these smaller types are going to be reused frequently. There are cases when we need to use a part of a type just once and it doesn't make much sense to move that part into a separate type.
Also, the lookup type is useful when we need to reuse a part of some type that we cannot touch, like, for example, a type from a third-party library.
The keyof Operator
The keyof
operator is used to query the names of the properties of a type and represent them as a union (key = property name):
interface User {
id: number;
name: string;
}
type UserProperties = keyof User; // "id" | "name"
So, the UserProperties
type is a union of properties that are present in the User interface.
Also, the type keyof T
is a subtype of string
:
let userProperty: UserProperties = 'id';
let someString: string = userProperty; // OK
Assigning keyof T
to a string works, but, assigning any string
to keyof T
doesn't:
userProperty = someString; // Error
Lookup Types + keyof Operator + Generics
Describing Access to Any Property in a Given Object
We can use the lookup type together with the keyof
operator, for example, in order to describe a function that reads the value of a property from an object:
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
TypeScript infers the return type of this function to be T[K]
and when we will call this function TypeScript will infer the actual type of the property that we're going to read:
let user = { name: 'John Doe', age: 25 };
let name = getProperty(user, 'name'); // string
let age = getProperty(user, 'age'); // number
The name
variable is inferred to be a string
and age - a number
.
Also, TypeScript will produce an error if you try to assign anything other than a "name" or "age" to the key
parameter in the getProperty
function:
let age = getProperty(user, 'nonexistentProperty'); // Error
Inferring the Type of a Parameter Based on Another Parameter in a Function
A similar pattern is used to describe document.addEventListener
in the DOM library included with TypeScript (lib.dom.d.ts):
// I shortened the original declaration
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (..., ev: DocumentEventMap[K]) => any, ...): void;
This pattern allows TypeScript to infer the type of the event object ev
that is passed to the listener
callback, based on the type of the event - K
. For example, for the event type "click", the event object in the callback should be of type MouseEvent
:
document.addEventListener('click', (e) => {
// e is inferred to be MouseEvent
});
document.addEventListener('keypress', (e) => {
// e is inferred to be KeyboardEvent
});
This pattern looks useful, so I recreated a simple example:
interface MyMouseEvent {
x: number;
y: number;
}
interface MyKeyboardEvent {
key: string;
}
interface MyEventObjects {
click: MyMouseEvent;
keypress: MyKeyboardEvent;
}
function handleEvent<K extends keyof MyEventObjects>(
eventName: K,
callback: (e: MyEventObjects[K]) => void
) {
// ...
}
handleEvent('click', (e) => {
// e is inferred to be MyMouseEvent
});
I created two types to describe two different event objects: MyMouseEvent
and MyKeyboardEvent
. Then, I created MyEventObjects
type to map event names to the corresponding event objects. And I created a generic function called handleEvent
, that allows to register a callback for a specific event.
Then I tried to implement the handleEvent
function:
function handleEvent<K extends keyof MyEventObjects>(
eventName: K,
callback: (e: MyEventObjects[K]) => void
) {
if (eventName === 'click') {
// Here, I expected e to be MyMouseEvent
callback({ x: 0, y: 0 }); // ERROR
} else if (eventName === 'keypress') {
// Here, I expected e to be MyKeyboardEvent
callback({ key: 'Enter' }); // ERROR
}
}
Basically, I tried to narrow the parameter of the callback to a more specific type.
When I checked whether the event name is "click", I expected TypeScript to infer the parameter of the callback to be MyMouseEvent
, because TypeScript infers the type of this parameter correctly when the handleEvent
function is called (check the earlier example).
Basically, inside of the "click" if block I told TypeScript that generic type parameter K
is equal to "click" and expected TypeScript to substitute "click" for the parameter K
in the declaration of the callback: callback: (e: MyEventObjects["click"]) => void
. But, this didn't happen, because TypeScript didn't recognise the relationship between eventName: K
and callback: (e: MyEventObjects[K]) => void
.
Then, I figured out that TypeScript infers the type of the callback's parameter e
to be an intersection(&
) of MyMouseEvent
and MyKeyboardEvent
:
e: MyEventObjects[K] >>>> e: MyMouseEvent & MyKeyboardEvent
And it doesn't narrow this type down to a more specialised type after the parameter K
becomes known inside of the function.
So, to fix the errors we'd have to use an assertion:
if (eventName === 'click') {
callback({ x: 0, y: 0 } as MyEventObjects[K]);
} else if (eventName === 'keypress') {
callback({ key: 'Enter' } as MyEventObjects[K]);
}
And that's how it works, at least at the moment.
Finally, let's have a look at the following code:
type JustObjects = MyEventObjects[keyof MyEventObjects]; // MyMouseEvent | MyKeyboardEvent
Here, JustObjects
is a union of the types of the values of MyEventObjects
interface.
Top comments (0)