Un arreglo de objetos genéricos en TypeScript
type Foo<T = unknown> = {
bar: T
bar2: (v: T) => T
}
const f1 : Foo<number> = {
bar: 42,
bar2: (v: number) => 5 + v
}
const f2 : Foo<string> = {
bar: "hello",
bar2: (v: string) => v
}
// a function that takes an array of any Foo
function process: (Foo|string)[]) {
...
}
process([f1, f2]) // ERROR
Da este error:
Type 'Foo<number>' is not assignable to type 'string | Foo<unknown>'.
Type 'Foo<number>' is not assignable to type 'Foo<unknown>'.
Types of property 'bar2' are incompatible.
Type '(v: number) => number' is not assignable to type '(v: unknown) => unknown'.
Types of parameters 'v' and 'v' are incompatible.
Type 'unknown' is not assignable to type 'number'
enlace del playground de TypeScript
No me queda claro cómo llegar a mi objetivo: tener una función que acepte un arreglo de cualquier forma de Foo para cualquier T.
Respuesta #1
Una forma de obtener este comportamiento específicamente para arrays de entradas [Foo<T0>, Foo<T1>, ..., Foo<TN>] es hacer que tu función sea genérica en el tipo de tupla T de los argumentos de tipo para cada elemento. Es decir, T sería [T0, T1, ..., TN]. Y harías que la entrada de tu función sea un tipo de tupla mapeado:
function extractValues<T extends any[]>(
ar: [...{ [I in keyof T]: Foo<T[I]> | string }]
) {
}
Aquí el parámetro ar tiene un tipo de tupla variádica que asigna cada elemento de la tupla T a un tipo Foo correspondiente (o un string supongo, como en tu ejemplo).
Veámoslo en acción:
extractValues([f1, f2, "okay"]) // okay
// function extractValues<[number, string, unknown]>
Eso funciona; el compilador infiere que T es el tipo de tupla [number, string, unknown], y por lo tanto que ar debería ser del tipo [Foo<number> | string, Foo<string> | string, Foo<unknown> | string], lo cual es. Nota que number y string se infieren a partir del tipo mapeado. El tercer elemento de la tupla no puede inferirse bien y vuelve a unknown, pero eso está bien porque es un string.
¿Qué pasa si ingresamos un dato incorrecto?
extractValues([{ bar: 1, bar2: (v: string) => v.toUpperCase() }, f1, f2]); // error
// --------------------> ~~~~
// function extractValues<[string | number, number, string]>
Aquí T se infiere como [string | number, number, string]. Y mientras que f1 y f2 están bien, el valor pasado como el primer elemento no lo está, ya que bar2 no es del tipo (v: number | string) => number | string
Así que eso también funciona; muestra que el compilador realmente restringe las entradas de una manera que Array<Foo<any>> no lo hace.
Enlace de Playground al código
Respuesta #2
Dado que quieres que el argumento de la función siga siendo genérico, necesitarás hacer que la función en sí misma sea genérica (más información aquí)
Entonces sería algo así
// a function that takes an array of any Foo
function extractValues<T>(ar: Foo<T>[]) {
// ...
}
extractValues([f1]) // this will now work
Si deseas que el array acepte tipos mixtos de Foo, puedes tiparlo así
function extractValues(ar: (Foo<number> | Foo<string>)[]) {
// ...
}
extractValues([f1, f2]) // you can now pass a heterogeneous array
Si absolutamente debes, esto aceptará cualquier tipo de foo. Pero usar any va en contra del propósito de TypeScript, por lo que se considera un mal olor de código
function extractValues(ar: Foo<any>[]) {
// ...
}
extractValues([f1, f2]) // you can now pass a heterogeneous array
