Tipo de retorno mapeado de Typescript: intersección reducida a nunca, propiedad de intersección en conflicto
Objetivo: hacer que el tipo de retorno de una función se base en el tipo de entrada utilizando un tipo mapeado para buscar el tipo de retorno.
Problema: Recibo un error debido a los conflictos en la intersección de tipos de unión discriminada, lo cual entiendo conceptualmente, pero no sé cómo estructurar mis tipos para lograr mi objetivo.
Type 'Integration' is not assignable to type 'IntegrationTypeData[T]'.
Type 'IntegrationA' is not assignable to type 'IntegrationTypeData[T]'.
Type 'IntegrationA' is not assignable to type 'never'.
The intersection 'IntegrationA & IntegrationB' was reduced to 'never' because property 'name' has conflicting types in some constituents.
Mi tipo se ve así:
enum Integrations {
A = 'A',
B = 'B',
}
type IntegrationMap<M extends { [key: string]: any }> = {
[Key in keyof M]: M[Key]
};
type IntegrationA = {
name: Integrations.A,
propertyA: string;
}
type IntegrationB = {
name: Integrations.B,
propertyB: number;
}
type IntegrationData = {
[Integrations.A]: IntegrationA;
[Integrations.B]: IntegrationB;
};
// this resolves to type Integration = IntegrationA | IntegrationB
type Integration = IntegrationMap<IntegrationData>[keyof IntegrationMap<IntegrationData>];
// the function I want to make have a dynamic/mapped return type
const getIntegration = <T extends Integrations>(name: T): IntegrationData[T] => {
// the type of the integrations variable is Integration[]
const integration = integrations.find((i: Integration) => i.name === name);
return integration;
};
// desired usage
// typeof intB = IntegrationB
const intB = getIntegration(Integrations.B);
¿Alguien puede ayudarme a entender cómo resolver este error y cómo escribir correctamente los datos para obtener un tipo de retorno dinámico y seguro para esta función?
Respuesta #1
Los elementos del arreglo integrations son del tipo unión (IntegrationA | IntegrationB).
Si miras la firma de llamada predeterminada para el método find() de los arrays, verás que devuelve un valor de tipo del elemento (IntegrationA | IntegrationB en este caso) o undefined. Por lo tanto, estás devolviendo un valor de tipo IntegrationA | IntegrationB | undefined. Pero dado que el tipo de retorno de tu función es IntegrationData[T] donde T es el tipo del argumento name pasado. El compilador no puede asegurar que lo primero sea asignable a lo segundo, por lo que genera una advertencia.
El mensaje de error en particular que recibes habla sobre el tipo de intersección IntegrationA & IntegrationB que se reduce al tipo never porque la única manera de estar seguro de que estás retornando un Integration[T] independientemente de T es si tu valor fuera tanto un IntegrationA y un IntegrationB, lo cual es imposible. Probablemente sea mejor no obsesionarse demasiado con el mensaje de error.
Basta con decir que el problema aquí es que el tipo de retorno de find() es demasiado amplio para tus propósitos.
Afortunadamente para ti, existe otra firma de llamada para find() que tiene el potencial de devolver un tipo más específico que el tipo del elemento del array (pero todavía posiblemente undefined). Si la función de devolución de llamada que pasas es una función de guarda de tipo definida por el usuario con un predicado de tipo de retorno de la forma x is Y, entonces devolverá el tipo más específico Y | undefined.
Sería bueno si el compilador pudiera entender que i => i.name === name actúa como una función de guarda de tipo con la firma de llamada (i: Integration) => i is IntegrationData[T]. Desafortunadamente, esto no sucede; TypeScript no infiere firmas de llamada de funciones de guarda de tipo a partir de implementaciones. Hay una solicitud abierta en microsoft/TypeScript#38390 para admitir esto en algunos casos, pero por ahora no forma parte del lenguaje. (Y aun si lo fuera, tal inferencia en esta situación podría estar más allá de sus capacidades.)
Si quieres usar i => i.name === name como una función de protección de tipos, necesitas anotarla como tal:
const getIntegration = <T extends Integrations>(name: T): IntegrationData[T] => {
const integration = integrations.find(
(i: Integration): i is IntegrationData[T] => i.name === name
// annotated ---> ^^^^^^^^^^^^^^^^^^^^^^^^^
);
return integration!;
};
La anotación te lleva la mayor parte del camino. Sin embargo, todavía no elimina undefined. Para todo lo que el compilador sabe, integrations en realidad no tiene un elemento del tipo IntegrationData[T]. Así que integration podría ser undefined. Puedes manejar esto explícitamente, escribiendo if (!integration) throw new Error("OH NO ME EQUIVOQUÉ") antes de return. O, si estás seguro de que en realidad no será undefined, puedes indicarle al compilador eso usando un operador de aserción de no nulo (!). Al escribir return integration!, estás afirmando que integration no será undefined ni null, y así el valor de tipo IntegrationData[T] | undefined puede tratarse como un IntegrationData[T].
Y ahora se compila sin errores.
Enlace de Playground al código
