Búsqueda de sitios web

tipos - El argumento de la función de mapeador de datos de TypeScript infiere nunca


Considera el siguiente ejemplo:

type columns = {
    A: number;
    B: string;
    C: boolean;
};
const mapper: { [T in keyof columns]: (item: columns[T]) => string } = {
    A: item => `${item}`,
    B: item => item,
    C: item => (item ? "Good" : "Bad"),
};
const data: columns[] = [
    { A: 0, B: "Hello", C: true },
    { A: 1, B: "World", C: false },
];
const keys: (keyof columns)[] = ["A", "B", "C"];
data.map(item => keys.map(key => mapper[key](item[key] as never)));
// should return [["0", "Hello", "Good"], ["1", "World", "Bad"]]

En la última línea, key es del tipo keyof columns, es decir, "A" | "B" | "C", lo que hace que mapper[key] se reduzca a (item: number & string & boolean) => string, es decir, (item: never) => string. (Señala si mi concepto está equivocado.)

Entonces la pregunta es, ¿cómo puedo reescribir el código de manera que item[key] no necesite ser convertido a never?

Respuesta #1

Esta es una limitación general de TypeScript y el tema de microsoft/TypeScript#30581. El compilador realmente no puede examinar una sola expresión como mapper[key](item[key]) y usar el análisis de flujo de control para analizarla y ver que es seguro.

El problema es que mapper[key] y item[key] son ambos de tipos unión. El tipo de mapper[key] es ((item: number) => string) | ((item: string) => string) | ((item: boolean) => string) y el tipo de item[key] es number | string | boolean. Pero el compilador no tiene una buena manera de rastrear la correlación entre los tipos de esos valores. Trata todas las uniones como esencialmente independientes entre sí. Sabemos que mapper[key] es (item: number) => string exactamente cuando item[key] es number, pero el compilador no lo sabe. Por todo lo que entiende, mapper[key] podría aceptar un number mientras que item[key] es string. La correlación tiene que ver con el hecho de que estamos usando la misma key en ambas expresiones, pero el compilador solo rastrea el tipo de key, y no su identidad.

Cuando tratas mapper[key] y item[key] de manera independiente, estás algo atrapado. Solo puedes llamar a una unión de tipos de funciones con argumentos que funcionarían para cada miembro de la unión. Es decir, una intersección de los parámetros... así que el compilador ve mapper[key] como asignable a (item: number & string & boolean) => string, también conocido como (item: never) => string... lo que significa que tal función nunca es segura de llamar en general.

Hasta que y a menos que haya alguna manera de indicarle al compilador que rastree las correlaciones entre expresiones de tipo unión, no hay una gran manera de proceder. Si te importa la seguridad de tipos por encima de todo, puedes escribir algún código redundante para lograrlo:

data.map(item => keys.map(key =>
  key === "A" ? mapper[key](item[key]) :
    key === "B" ? mapper[key](item[key]) :
      mapper[key](item[key])
)); // no error but it's redundant and repetitive 
// and also redundant

Si te importa la conveniencia en lugar de la seguridad de tipos, entonces puedes usar una aserción de tipo para suprimir errores. Tu ejemplo de item[key] as never es una forma de hacerlo, aunque técnicamente estás mintiendo sobre lo que item[key] es. Si no quieres mentir, puedes usar una función de devolución de llamada genérica como esta:

data.map(item => keys.map(<K extends keyof Columns>(key: K) =>
  (mapper[key] as (item: Columns[K]) => string)(item[key]) // okay
));

Tienes que afirmar que mapper[key] es un valor del tipo (item: Columns[K]) => string) porque el compilador no puede verificar eso, aunque teóricamente debería poder. Resuelve ansiosamente mapper[key] a una unión de funciones cuando intentas llamarlo. Y dado que mapper[key] realmente es un valor de ese tipo, no estamos mintiendo. La falta de seguridad de tipos aquí proviene del hecho de que si alguien malicioso cambiara las entradas de mapper, el compilador no se daría cuenta:

const evilMapper = {
  A: mapper.B,
  B: mapper.C,
  C: mapper.A
}

data.map(item => keys.map(<K extends keyof Columns>(key: K) =>
  (evilMapper[key] as (item: Columns[K]) => string)(item[key]) // okay?!
));

mientras que la versión redundante de manera redundante empezaría a gritar al respecto:

data.map(item => keys.map(key =>
  key === "A" ? evilMapper[key](item[key]) : // error
    key === "B" ? evilMapper[key](item[key]) : // error
      evilMapper[key](item[key]) // error
)); 

Enlace de Playground al código