Tipo de intersección de matrices en TypeScript: la propiedad no existe cuando se accede en Array.forEach, Array.some, etc., pero es accesible dentro de un bucle for
Entonces, puede que no tenga la terminología correcta para describir este problema, lo que hizo que buscarlo fuera complicado, pero espero que mi ejemplo ayude a aclararlo.
Contexto: Estoy usando selectores de datos de consulta en react-query para preprocesar los resultados de la consulta y adjuntar algunas propiedades que necesito globalmente en la aplicación. Me estoy encontrando con un problema con los tipos producidos, que logré reducir y reproducir fuera de react-query mismo.
Aquí está el código de reproducción (también en TS Playground)
// Data structure coming from third-party code (I have no control over it)
// Simplified for a more concise reproduction:
type DataStructure =
& Array<{ a: number; b: number }>
& Array<{ b: number; c: number }>
const structure: DataStructure = [
{
a: 1,
b: 2,
c: 3,
},
]
structure[0].a // ok
structure[0].b // ok
structure[0].c // ok
for (const s of structure) {
s.a // ok
s.b // ok
s.c // ok
}
structure.forEach(s => {
s.a // ok
s.b // ok
s.c // Property 'c' does not exist on type '{ a: number; b: number; }'.(2339)
})
// If we had control over DataStructure, we'd write it like this instead:
// Array<{ a: number; b: number } & { b: number; c: number }>
La DataStructure es un tipo de intersección de dos Arrays con tipos de elementos parcialmente superpuestos.
Como se demuestra en el código y los comentarios, las 3 propiedades están disponibles cuando se accede a los elementos del array ya sea por su índice o dentro de un bucle for, pero dentro de un bucle forEach (o cualquier método de array como some, every, etc.) solo están disponibles las propiedades del primer tipo de array (de la intersección).
Intentando acceder a c dentro del bucle forEach, TypeScript se queja:
Property 'c' does not exist on type '{ a: number; b: number; }'.(2339)
Ahora, si tuviera control sobre la definición de la estructura de datos, la describiría así:
type DataStrcture = Array<{ a: number; b: number } & { b: number; c: number }>
Eso de hecho resolvería el problema. Pero no tengo control sobre esa parte del código.
Lo que más me interesa es entender POR QUÉ TypeScript se comporta de esta manera aquí. Eso es lo que más me desconcierta, ¡pero si alguien también puede ofrecer una solución limpia, sería aún más increíble!
Respuesta #1
Esto se considera una limitación de diseño de TypeScript. Las intersecciones de tipos de matrices se comportan de manera extraña y no se recomiendan. Consulte microsoft/TypeScript#41874 para una respuesta autorizada. Dice:
La intersección de arrays es extraña ya que existen muchas invariantes de los arrays y muchas invariantes de las intersecciones que no pueden cumplirse simultáneamente. Por ejemplo, si es válido llamar a
a.push(x)cuandoaesA, entonces debería ser válido escribirab.push(x)cuandoabesA & B, pero eso crea una lectura no segura en(A & B)[number].En orden superior, el comportamiento actual es realmente lo mejor que podemos hacer; en orden cero, es realmente preferible simplemente escribir
Array<A & B>,Array<A> | Array<B>oArray<A | B>dependiendo de lo que realmente quieras que ocurra.
Parte de la rareza se debe al hecho de que las intersecciones de funciones y métodos se comportan como sobrecargas, y los arreglos se consideran de manera insegura covariantes en su tipo de elemento (ver ¿Por qué los arreglos de TypeScript son covariantes?), lo que significa que de repente tienes la situación con push() como se describió anteriormente.
Ocurren otras rarezas cuando intentas iterar a través de ellos, como has mostrado, y como se describe en microsoft/TypeScript#39693.
Entonces, el enfoque recomendado es evitar las intersecciones de arreglos y, en su lugar, usar arreglos de intersecciones si eso es lo que deseas. Si puedes escribir eso directamente, deberías hacerlo. Si tienes un tipo con arreglos intersecados anidados, puedes mirar ¿Por qué no funciona la fusión de 2 tipos con un nombre de propiedad compartido al crear un tipo con esa propiedad a partir del tipo fusionado? para un posible enfoque de cómo escribir un tipo de utilidad para tratar con esos.
Como mencioné en el comentario, si no puedes controlar el tipo de datos, deberías mostrar esto a quien lo haga para que puedan arreglarlo. De lo contrario, necesitarás buscar una solución alternativa traduciendo entre la definición de tipo externa y tu versión corregida de la misma.