Home > Net >  Typescript generics: checking a unique property on a generic type object in if condition doesn'
Typescript generics: checking a unique property on a generic type object in if condition doesn'

Time:11-23

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.

ts playground

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
    }
});

See Playground

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.

Playground

  • Related