Quiero escribir una función genérica que acepte un número variable de argumentos que pueden tener diferentes tipos y devuelva una tupla basada en esos argumentos.
Aquí hay un ejemplo en JavaScript:
function evaluate (...fns) { return fns.map(fn => fn()) } evaluate( () => 10 ) // [ 10 ] evaluate( () => 10, () => 'f', () => null ) // [ 10, 'f', null ]Y en TypeScript necesito convertir de alguna manera la tupla del argumento extendido en una resultante:
function evaluate<T1, T2 ... Tn> ( ...fns: [() => T1, () => T2 ... () => Tn] ): [T1, T2 ... Tn] { return fns.map(fn => fn()) as [T1, T2 ... Tn] } evaluate( () => 10 ) // [ 10 ]: [number] evaluate( () => 10, () => 'f', () => null ) // [ 10, 'f', null ]: [number, string, null]Probé un enfoque ingenuo de crear una sobrecarga para todas las longitudes razonables de tupla:
function evaluate<T1> ( fn1: () => T1 ): [T1] function evaluate<T1, T2> ( fn1: () => T1, fn2: () => T2 ): [T1, T2] function evaluate<T1, T2, T3> ( fn1: () => T1, fn2: () => T2, fn3: () => T3 ): [T1, T2, T3] function evaluate<T1, T2, T3> ( ...fns: Array<(() => T1) | (() => T2) | (() => T3)> ): [T1] | [T1, T2] | [T1, T2, T3] { return fns.map(fn => fn()) as [T1] | [T1, T2] | [T1, T2, T3] }Pero se ve horrible, no escala bien y causa problemas con un cuerpo funcional más complejo.
¿Hay alguna manera de que esto se pueda hacer dinámicamente? ¡Gracias!
La forma más fácil de implementar esto es hacer que la evaluate() sea genérica en su tipo de salida tipo matriz T (que pretende ser un tipo de tupla ) y luego representar el parámetro rest de fns como un tipo asignado en T , teniendo en cuenta que los tipos de matriz/tupla asignados son también tipos de matriz/tupla :
function evaluate<T extends any[]>( ...fns: { [I in keyof T]: () => T[I] } ) { return fns.map(fn => fn()) as T; } Tenga en cuenta que la aserción de tipo as T es necesaria porque el compilador no puede ver que fns.map(fn => fn()) tendrá el efecto de convertir una matriz/tupla de tipos de funciones en la matriz/tupla de tipos de devolución correspondientes. Consulte Asignación de valores con tipo de tupla a diferentes valores con tipo de tupla sin conversiones para obtener más información.
Debido a que {[I in keyof T]: () => T[I]} es un tipo mapeado homomórfico donde estamos mapeando directamente sobre keyof T (consulte ¿Qué significa "tipo mapeado homomórfico"? para obtener más información), el compilador es capaz de inferir T de él (la página vinculada está en desuso, pero aún es precisa y no existe una nueva página 🤷♂️).
Veámoslo en acción:
const x = evaluate(() => 10); // const x: [number] const y = evaluate( () => 10, () => 'f', () => null ) // const y: [number, string, null] Se ve bien. El compilador ve que x es de tipo [number] e y es de tipo [number, string, null] . También se comporta razonablemente en los casos en los que pasa un argumento de reposo de orden/longitud desconocidos:
const fs = [() => "a", () => 3]; // const fs: ((() => string) | (() => number))[] const z = evaluate(...fs); // const z: (string | number)[] Aquí fs es del tipo Array<(()=>string) | (()=>number)> , por lo que z es del tipo análogo Array<string | number> .