2

I can't seem to match the types of the param the function takes and the payload:

type SelectCase = {
  type: 'selectCase';
}

type UpdateNotification = {
  type: 'updateNotification';
  payload: {
    title: string;
    message: string;
  }
}

type Message = SelectCase | UpdateNotification;

const someFn = <TMessageType extends Message['type']>(
    messageType: TMessageType,
    onMessage: (
        payload?: Extract<Message, { type: TMessageType }> extends {
            payload: infer TPayload;
        }
            ? TPayload
            : undefined
    ) => void
) => {
  const handleMessage = (message: Message) => {
    if(message.type === messageType) {
      // does not work
      onMessage(message.payload);
    }
  }
};

This is a reduced example, I have more than 20 types of messages. Any idea why this does not work?

I have tried type predicates and call signatures but it still errors.

f0nt41n3
  • 23
  • 3
  • I'm playing around with this a little and having some trouble, so i'll follow up when I find an answer. In the mean time, this thread is proving helpful https://stackoverflow.com/questions/50870423/discriminated-union-of-generic-type – ballmerspeak Nov 14 '22 at 17:59
  • Hmm, this is sort of a nightmare version of the issue in [ms/TS#30581](https://github.com/microsoft/TypeScript/issues/30581) and I can't figure out how to address it with the fix mentioned in [ms/TS#47109](https://github.com/microsoft/TypeScript/pull/47109). As a workaround you could write a type guard function like [this](https://tsplay.dev/WkkQPW); does that work for you or am I missing something? (Please mention @jcalz in your reply to notify me) – jcalz Nov 14 '22 at 19:17
  • Thanks for the comments! I saw that thread but I don't believe it's directly related, at least I was not able to make it work. @jcalz I try to make something similar to ms/TS#47109, but it still throws an error :( https://tsplay.dev/wEBdVN – f0nt41n3 Nov 14 '22 at 21:59
  • Right, I suspect the ms/TS#47109 solution will not let you avoid a type guard function, since there's no good way to relate `Message` and `Message

    ` for two independent generic types `K` and `P`. [This](https://tsplay.dev/w6vYvw) is the closest I can get, and it's really the same as my earlier solution except that it tries to use ms/TS#47109. So, shall I write up an answer showing how to write a type guard function to deal with this? Or do you think I'm still missing something about the question? (Pls mention @jcalz to notify me, thanks)

    – jcalz Nov 15 '22 at 00:32
  • @jcalz Both of your solutions seems the be working great, the only issue is that it does not error when you call someFn with a function with an incorrect type, take a look at this https://tsplay.dev/mbQq4N It also happens with the other solution. – f0nt41n3 Nov 15 '22 at 14:17
  • I don't see you calling it incorrectly, please see [this FAQ entry](https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-functions-with-fewer-parameters-assignable-to-functions-that-take-more-parameters). Even so it looks like you might be asking a followup question which is out of scope here. I will write up an answer when I get a chance. – jcalz Nov 15 '22 at 14:25
  • @jcalz Awesome, thank you for the info, I didn't know that. Thank you for your help! Do you have to answer so I can mark your answer as correct? I don't think I can do that with a comment. – f0nt41n3 Nov 15 '22 at 14:43

1 Answers1

1

The problem here is TypeScript's lack of direct support for correlated union types as described in microsoft/TypeScript#30581. Essentially the type checker cannot easily be made to understand that the check message.type === messageType should narrow message to the member of the Message union whose payload property can be handled by onMessage(). The type of message becomes correlated to the type of messageType after that check without being known specifically. But there's no easy way to convey such information to the compiler.

Often you can work around this issue with generics and some refactoring of types as described in microsoft/TypeScript#47109, but I couldn't get that to work here (if someone does get this to work, let me know).


So instead, I think the best thing we can do is write a custom type guard function to say that message.type === messageType will narrow message appropriately. Here's one way to do it:

First, I'm going to copy your conditional type for onMessage()'s parameter into a utility type we can reuse:

type CorrespondingPayload<K extends Message['type']> =
    Extract<Message, { type: K }> extends {
        payload: infer TPayload;
    } ? TPayload : undefined

And now the custom type guard function looks like this:

function sameType<K extends Message['type']>(
    message: Message, messageType: K
): message is Message & { type: K, payload: CorrespondingPayload<K> } {
    return message.type === messageType;
}

If sameType(message, messageType) returns true, then message will be narrowed to a type where payload is known to be of type CorrespondingPayload<K>. Let's use it:

const someFn = <K extends Message['type']>(
    messageType: K,
    onMessage: (payload?: CorrespondingPayload<K>) => void
) => {
    const handleMessage = (message: Message) => {
        if (sameType(message, messageType)) {
            onMessage(message.payload); // okay
        }
    }
};

Looks good!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360