Búsqueda de sitios web

typescript - Coincidencia de tipos en uniones discriminadas


Parece que no puedo hacer coincidir los tipos del parámetro que la función acepta y la carga útil:

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

Este es un ejemplo reducido, tengo más de 20 tipos de mensajes. ¿Alguna idea de por qué esto no funciona?

He intentado predicados de tipo y firmas de llamada, pero todavía da error.

Respuesta #1

El problema aquí es la falta de soporte directo de TypeScript para tipos de unión correlacionados como se describe en microsoft/TypeScript#30581. Esencialmente, no se puede hacer fácilmente que el verificador de tipos entienda que la comprobación message.type === messageType debería restringir message al miembro de la unión Message cuyo atributo payload puede ser manejado por onMessage(). El tipo de message se vuelve correlacionado con el tipo de messageType después de esa comprobación, sin ser conocido específicamente. Pero no hay una manera fácil de transmitir esa información al compilador.

A menudo puedes sortear este problema con genéricos y algo de refactorización de tipos como se describe en microsoft/TypeScript#47109, pero no pude hacer que esto funcionara aquí (si alguien lo logra, avísenme).


Así que, en cambio, creo que lo mejor que podemos hacer es escribir una función de guardia de tipo personalizada para indicar que message.type === messageType reducirá message de manera adecuada. Aquí hay una forma de hacerlo:

Primero, voy a copiar tu tipo condicional para el parámetro de onMessage() en un tipo de utilidad que podamos reutilizar:

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

Y ahora la función de guardia de tipo personalizada se ve así:

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

Si sameType(message, messageType) devuelve true, entonces message se reducirá a un tipo donde se sabe que payload es del tipo CorrespondingPayload<K>. Usemoslo:

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

¡Se ve bien!

Enlace de Playground al código