Búsqueda de sitios web

typescript - Cómo evitar los tipos condicionales distributivos


Estos son los tipos que tengo:

type Action<T> = T extends undefined ? {
    type: string;
} : {
    type: string;
    payload: T;
}

type ApiResponse<T> = {
    ok: false;
    error: string;
} | {
    ok: true;
    data: T;
};

He definido esta función:

function handleApiResponse<T>(apiResponse: ApiResponse<T>) {
    const a: Action<ApiResponse<T>> = {
        type: "response",
        payload: apiResponse,
    }
}

El problema es que a tiene un error porque el tipo condicional en Action se distribuye sobre ApiResponse.

Lo que necesito de Action<T> es un tipo que literalmente es lo que dice, es decir, hay dos casos:

  1. El parámetro de tipo pasado a Action<T> no está definido, en cuyo caso Action<undefined>={ type: string }
  2. El parámetro de tipo pasado a Action<T> es cualquier otra cosa, en cuyo caso Action<T>={ type: string, payload: T }

Pero esto no funciona cuando T es un tipo unión debido a los tipos condicionales distribuidos.

¿Cómo puedo definir un tipo así que funcione incluso cuando T es una unión?

Respuesta #1

Como mencionaste, tu definición de Action<T> es un tipo condicional distributivo, en el cual las uniones en T se dividen en sus miembros individuales (por ejemplo, B | C | D), se pasan a través de Action<T>, y el resultado se vuelve a poner en una nueva unión (por ejemplo, Action<B> | Action<C> | Action<D>). Esto suele ser el comportamiento que la gente espera de sus tipos condicionales, pero no es lo que quieres aquí; es decir, has usado un tipo condicional distributivo accidentalmente.

Un tipo condicional de la forma TTT extends UUU ? VVV : WWW solo será distributivo si el tipo TTT que estás comprobando es un parámetro de tipo genérico desnudo, como el T en tu definición de Action<T>. Si es un tipo específico (como string o Date), no será distributivo. Y si es una expresión más complicada que involucra un parámetro de tipo (como {x: T}), no será distributivo. A veces me refiero a esta última idea, donde tomas el parámetro de tipo "desnudo" T y haces algo con él como {x: T}, como "vestir" el parámetro de tipo.


Entonces tienes T extends undefined, que es distributivo:

type Action<T> = T extends undefined ? ... : ...

Si quieres desactivar eso, necesitas reformular la verificación de manera que T esté encapsulado pero mantenga el mismo comportamiento (así que la verificación tiene éxito si y solo si T extiende a undefined). La forma más fácil de hacer esto es encapsular ambos lados de la cláusula extends en un tipo de tupla de un solo elemento), así T se convierte en [T] y undefined se convierte en [undefined]:

type Action<T> = [T] extends [undefined] ? ... : ...

y entonces tu código funciona:

function handleApiResponse<T>(apiResponse: ApiResponse<T>) {
  const a: Action<ApiResponse<T>> = { 
    type: "response",
    payload: apiResponse,
  } // okay
}

Tenga en cuenta que TypeScript considera que las tuplas y los arrays son covariantes en sus tipos de elementos, lo que significa que Array<X> extiende Array<Y> o [X] extiende [Y] si y solo si X extiende Y. Esto es técnicamente inseguro de hacer (consulte esta pregunta y sus respuestas para más información), pero muy conveniente.


Así que esa es una regla general que puedes usar: si tienes TTT extends UUU ? VVV : WWW y accidentalmente es distributivo, puedes desactivarlo escribiendo [TTT] extends [UUU] ? VVV : WWW. Esto se menciona al final de la documentación sobre tipos condicionales distributivos, aunque solo dice algo como "haz esto" y no por qué funciona. Puede parecer que los corchetes son algún tipo de sintaxis especial diseñada para tipos condicionales... pero no lo son. Simplemente se está usando la sintaxis existente de tupla de un solo elemento, que resulta hacer lo que queremos sin requerir demasiados caracteres adicionales.

Enlace de Playground al código

Artículos relacionados