Reputation: 1189
I am trying to write the equivalent of a message subscription function. A simplified version without types looks like this:
function newMessage(message) {
postMessage(message)
}
function subscribe(messageType, callback) {
handleNewMessage(message => {
if (message.type === messageType) {
callback(message.data)
}
})
}
newMessage
's message
can be multiple types. Therefore I'm defining them separately, then using a discriminated union to type the message
argument. An example:
type FooMessage = {
type: 'foo',
data: {
a: string,
b: number
}
}
type BarMessage = {
type: 'bar'
}
type MessageType = FooMessage | BarMessage
Note how BarMessage
does not have any data associated.
This allows my newMessage
function be typed like this:
function newMessage(message:MessageType):void {
postMessage(message)
}
I am not entirely sure how to type subscribe
though. This is what I have so far:
function subscribe(messageType: MessageType['type'], callback) {
handleNewMessage(message => {
if (message.type === messageType) {
callback(message.data)
}
})
}
I am not entirely sure how to type callback
. If I do MessageType['data']
I get an error since data
isn't always present. Even if I did define data:undefined
on BarMessage
, I have lost the link between the message type and the data.
Ideally, I would like to be able to write subscribe('bar', (data) => console.log(data))
and typescript to know that data
is actually undefined in this case.
One option I can see is to declare a function type multiple times for each message type I have, but this feels overly verbose since it means for each message type, I'm defining the MessageType
and the associated subscribe function.
In reality, I have many many more messages and would prefer not to do this.
What is the best way to type subscribe
's callback
function?
Upvotes: 1
Views: 115
Reputation: 2653
You can use the following:
T
so that TypeScript can narrow the message type if it's statically knownExtract
which allows narrowing to a particular member type of a distributed union, see this SO thread for more detailsdata
from a message only when its present as a keytype TypedMessage<T extends MessageType['type']> = Extract<MessageType, { type: T }>;
type MessageData<M extends MessageType> = M extends { data: any } ? M['data'] : undefined;
function subscribe<T extends MessageType['type']>(
messageType: T,
callback: (data: MessageData<TypedMessage<T>>) => void
) {
handleNewMessage(message => {
if (message.type === messageType) {
callback((message as { data?: any }).data)
}
})
}
subscribe('foo', (data) => console.log(data)); // typeof data === FooMessage['data']
subscribe('bar', (data) => console.log(data)); // typeof data === undefined
Here's a playground link TypeScript Playground
Upvotes: 2