Promisificar funciones con callbacks en TypeScript
Tengo una clase con múltiples sobrecargas en los métodos que aceptan callbacks. Quiero hacer que sean basados en Promise en su lugar, pero tengo problemas para hacerlo de manera segura en cuanto a tipos con genéricos de TypeScript.
Básicamente la API es esto:
class CallbackClient {
method(req: Request, callback: (error: Error, response: Response) => void): void
method(req: Request, options: Options, callback: (error: Error, response: Response) => void): void
}
y debería convertirse en esto:
class PromiseClient {
method(req: Request): Promise<Response>
method(req: Request, options: Options): Promise<Response>
}
Pero, ¿cómo defino el PromiseClient como un tipo genérico en TypeScript? En pseudocódigo quiero un tipo así:
type PromiseClient<T> = {
[P in keyof T]: (args of T[P] without the callback) => Promise<response of T[P] in callback>
}
Tengo una implementación que hace que esto suceda, pero pierdo la seguridad de tipos debido a mi incapacidad de poner esto en su lugar.
La función para mapearlo es así:
function toPromiseClient<T extends CallbackClient>(client: T): PromiseClient<T> {
const promiseClient: PromiseClient<T> = {}
Object.keys(Object.getPrototypeOf(client))
.forEach((functionName: string) => {
const originalFunction = client[functionName as keyof T]
if (!originalFunction) {
return
}
promiseClient[functionName as keyof T] = (...args: any[]) => new Promise<any>((resolve, reject) => {
if (args && args.length > 1 && typeof (args[args.length - 1]) === 'function') {
reject(new Error('Use Promise API instead of callbacks'))
return
}
originalFunction.call(client, ...args, (error: Error, res: any) => {
if (error) {
reject(error)
return
}
resolve(res)
})
})
})
return promiseClient
}
Respuesta #1
Esto es parcialmente posible en TypeScript 2.8 (todavía no lanzado, debería ser lanzado en marzo de 2018, puedes obtenerlo mediante npm install -g typescript@next)
La solución:
declare class CallbackClient {
method1(req: Request, options: Options, callback: (error: Error, response: Response) => void): void
method1(req: Request, callback: (error: Error, response: Response) => void): void
method2(req: Request, options: Options, callback: (error: Error, response: Response) => void): void
method3(req: Request, options?: Options, callback?: (error: Error, response: Response) => void): void
}
type Promisify<T> = {
[P in keyof T]:
T[P] extends (callback: (error: Error, response: infer TResponse) => void) => void ? () => Promise<TResponse> :
T[P] extends (p1: infer P1, callback: (error: Error, response: infer TResponse) => void) => void ? (p1: P1) => Promise<TResponse> :
T[P] extends (p1: infer P1, p2: infer P2, callback: (error: Error, response: infer TResponse) => void) => void ? (p1: P1, p2: P2) => Promise<TResponse> :
T[P] extends (p1: infer P1, p2: infer P2, p3: infer P3, callback: (error: Error, response: infer TResponse) => void) => void ? (p1: P1, p2: P2, p3: P3) => Promise<TResponse> :
never;
}
type PromiseClient = Promisify<CallbackClient>;
// Will be equivalent to :
// type PromiseClient = {
// method1: (p1: Request) => Promise<Response>; // Only one overload, not the second
// method2: (p1: Request, p2: Options) => Promise<Response>;
// method3: (p1: Request, p2: Options | undefined) => Promise<Response>;
// }
La solución anterior utiliza tipos condicionales para extraer los tipos de parámetros de cada método de la clase original, y luego utiliza los tipos para crear un nuevo tipo de función para el método que devuelve una promesa.
Las limitaciones:
- No funciona para un número arbitrario de parámetros, la solución anterior funciona para hasta 3 parámetros y el callback, pero puedes agregar más
- Los nombres de los parámetros se han perdido
- Si los parámetros son opcionales, se volverán obligatorios con su tipo siendo
ArgType | undefined - Si hay múltiples sobrecargas de un método, solo aquella con la menor cantidad de parámetros aparecerá en el resultado. En el ejemplo anterior, solo obtenemos
method1: (p1: Request) => Promise<Response>, no obtenemos tambiénmethod1: (p1: Request, p2: Options) => Promise<Response>(y dependiendo del orden de las sobrecargas, podrías obtener resultados extraños, es peculiar).
