¿Por qué TypeScript no puede inferir el parámetro de tipo genérico cuando un parámetro de tipo depende de otro?
Consideremos el siguiente código:
type SomeFunc<Arg, ReturnType> = (data: Arg) => ReturnType;
function wrapper<T extends SomeFunc<R, any>, R>(callbackOne: T, callbackTwo: (id: number, data: R) => void) {}
const callback: SomeFunc<number, void> = () => {};
// Works fine when types are provided manually
wrapper<SomeFunc<number, void>, number>(callback, (id, data) => {});
// TypeScript can't infer correct type here
wrapper(callback, (id, data) => {});
Enlace del parque
Como se puede ver arriba, tenemos un parámetro de tipo genérico que se utiliza en otro parámetro. En ese caso, TypeScript no es capaz de inferir el tipo correcto automáticamente, aunque parezca que tiene toda la información necesaria.
Una solución obvia aquí es eliminar el parámetro de tipo R y en su lugar confiar en T para el tipo de datos: data: Parameters<T>[0]. Sin embargo, T puede ser increíblemente complejo y extraer el tipo sería bastante difícil y frágil.
¿Por qué TypeScript no puede hacer eso y hay una mejor solución para esto que la mencionada arriba?
Respuesta #1
TypeScript no usa restricciones como sitios de inferencia para argumentos de tipo genérico al llamar funciones genéricas. Es una característica faltante, sugerida en microsoft/TypeScript#7234 pero nunca implementada.
Los argumentos de tipo solo se infieren a partir de los tipos de los parámetros de la función y, a veces, contextualmente a partir del tipo de retorno. Entonces, en la firma de la llamada:
declare function wrapper<T extends SomeFunc<R, any>, R>(
callbackOne: T,
callbackTwo: (id: number, data: R) => void
): void;
R se infiere a partir del tipo de callbackTwo y eso es todo. El hecho de que T esté limitado a SomeFunc<R, any> no afecta la inferencia. Solo sirve como una verificación de T, lo que significa que si R no se infiere como deseas, entonces es probable que T falle la verificación.
La sugerencia en microsoft/TypeScript#7234 de admitir el uso de restricciones como sitios de inferencia fue discutida y finalmente rechazada como innecesaria una vez que la inferencia a partir de tipos de intersección mejoró lo suficiente para permitir una solución alternativa. El enfoque consiste en reemplazar (o complementar) las restricciones en el sitio de declaración como X extends F<Y> con intersecciones en el sitio de uso como X & F<Y>. En tu ejemplo eso se vería así
function wrapper<T extends SomeFunc<R, any>, R>(
callbackOne: T & SomeFunc<R, any>,
// ----------> ^^^^^^^^^^^^^^^^^^
callbackTwo: (id: number, data: R) => void
) { }
La inferencia de T no se ve afectada, pero ahora R puede inferirse a partir de callbackOne y no solo de callbackTwo, y tu código funciona como se desea:
const callback: SomeFunc<number, void> = () => { };
wrapper(callback, (id, data) => { }); // okay
Enlace de Playground al código
