Typing postMessage
communication in my web app and currently stuck with trying to write Typescript types for window.addEventListener
listeners.
I have a list of possible messages my listeners can receive and I distinguish them by event.data.method
property. However with the types I wrote I'm not able to determine the rest of the event.data
content. Am I missing some type guards or my generics are just wrong? Thank you.
interface Messages {
'say-hi': {
greeting: string;
};
'say-bye': {
farewell: string;
};
}
type MessageListener = <M extends keyof Messages>(
e: MessageEvent<{
method: M;
data: Messages[M];
}>,
) => any;
const addMessageEventListener = (listener: MessageListener): void =>
window.addEventListener('message', listener);
addMessageEventListener(event => {
if (event.data.method === 'say-hi') {
// Here I would expect that I'll be able to use "event.data.data.greeting", but I get:
// Property 'greeting' does not exist on type '{ greeting: string; } | { farewell: string; }'.
// Property 'greeting' does not exist on type '{ farewell: string; }'.(2339)
console.log(event.data.data.greeting);
}
});
CodePudding user response:
Generics will get you in trouble here. If you never statically provide a type for a generic type parameter, then you don't need a generic. And here M
never gets populated by anything because the argument e
will never have any other type besides the union of possible event types.
What you really want is a discriminated union of possible message types.
You can derive that union from Messages
with a mapped type, like so:
type MyMessageEvent = {
[K in keyof Messages]: MessageEvent<{
method: K,
data: Messages[K]
}>
}[keyof Messages]
Which is equivalent to this, but will update if you change the Messages
type.
type MyMessageEvent =
| MessageEvent<{
method: 'say-hi'
data: { greeting: string }
}>
| MessageEvent<{
method: 'say-bye'
data: { farewell: string }
}>
This is now a discriminated union, where the discriminant method
allows you to narrow the type to just one of those union members.
And now you simply use that type like any other with no generics.
type MessageListener = (e: MyMessageEvent) => any;
const addMessageEventListener = (listener: MessageListener): void =>
window.addEventListener('message', listener);
addMessageEventListener(event => {
if (event.data.method === 'say-hi') {
console.log(event.data.data.greeting); // works
}
});
CodePudding user response:
I would use a more straightforward type definition personally:
type Messages = { method: 'say-hi', data: {greeting: string} }
| { method: 'say-bye', data: {farewell: string} }
type MessageListener = ( e: MessageEvent<Messages> ) => any;
const addMessageEventListener = (listener: MessageListener): void =>
window.addEventListener('message', listener);
addMessageEventListener(event => {
if (event.data.method === 'say-hi') {
console.log(event.data.data.greeting); // ok
}
});
Unless there's a reason to structure it the way you did, this seems simpler.