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 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
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
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.
Get this detailed online course and learn TypeScript and how to use it in React applications.