简体   繁体   中英

Discriminated unions type matching

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM