如何将回调函数转换为支持正确返回类型推断的async/await函数?
我明白你遇到的问题了——原来的awaitify用ReturnType<F>来推断Promise的返回类型,但其实像foo这类带回调的函数,它的返回值是void,所以类型推断完全不对。咱们一步步拆解,实现一个能正确识别回调中value类型的版本。
核心思路
我们的目标很明确:对于遵循**Node.js风格回调(最后一个参数是(err, value) => void)**的函数,awaitify(func, ...args)要返回Promise<Value>,其中Value是回调函数第二个参数的类型。要做到这一点,需要分三步提取类型:
- 从原函数的参数列表中,提取最后那个回调函数的类型
- 从回调函数类型中,提取成功值
value的类型 - 确保
awaitify接收的参数是原函数去掉回调后的所有参数
逐步实现与解释
1. 提取回调函数的类型
首先,我们需要从原函数F的参数里,拿到最后那个回调的类型。用TypeScript的条件类型和infer关键字可以轻松做到:
// 提取函数F的最后一个参数(即回调函数)的类型 type CallbackType<F extends (...args: any[]) => any> = Parameters<F> extends [...infer _RestArgs, infer Callback] ? Callback : never;
Parameters<F>会返回F的所有参数组成的数组类型[...infer _RestArgs, infer Callback]把数组拆分成“前面所有参数”(用_RestArgs忽略)和“最后一个参数”(即回调Callback)
2. 从回调中提取成功值的类型
拿到回调类型后,我们要提取它第二个参数(也就是成功时的value)的类型:
// 从回调函数类型中提取value的类型 type ResolvedValue<F extends (...args: any[]) => any> = CallbackType<F> extends (err: any, value: infer Value) => any ? Value : never;
- 这里检查回调是否符合
(err, value) => ...的结构,用infer Value捕获第二个参数的类型 - 如果回调不符合这个结构,就返回
never(相当于类型报错,提示输入不符合规范)
3. 定义awaitify的参数类型
我们需要让awaitify接收原函数去掉回调后的所有参数,所以定义一个类型来提取这些参数:
// 提取原函数去掉最后一个回调参数后的参数列表 type ArgsWithoutCallback<F extends (...args: any[]) => any> = Parameters<F> extends [...infer Args, any] ? Args : never;
这个和你原来的Head类型作用一致,但语义更清晰,一眼就能看出是“去掉回调后的参数”。
4. 完整的awaitify实现
把上面的类型整合起来,写出最终的函数:
type CallbackType<F extends (...args: any[]) => any> = Parameters<F> extends [...infer _RestArgs, infer Callback] ? Callback : never; type ResolvedValue<F extends (...args: any[]) => any> = CallbackType<F> extends (err: any, value: infer Value) => any ? Value : never; type ArgsWithoutCallback<F extends (...args: any[]) => any> = Parameters<F> extends [...infer Args, any] ? Args : never; export function awaitify<F extends (...args: any[]) => any>( func: F, ...args: ArgsWithoutCallback<F> ): Promise<ResolvedValue<F>> { return new Promise((resolve, reject) => { func(...args, (err: Error | null, value: ResolvedValue<F>) => { if (err) { reject(err); } else { resolve(value); } }); }); }
验证效果
用你给出的foo函数测试一下:
function foo(a: string | null, b: number | null, c: string[] | null, callback: (err: Error | null, value: string | number | string[]) => void) { if (a) { callback(null, a); } else if (b) { callback(null, b); } else if (c) { callback(null, c); } else { callback(new Error("error")); } } // 现在result会被正确推断为string | number | string[] const result = await awaitify(foo, null, null, ["1", "2", "3"]); // 当传入不同参数时,类型会自动缩小: const stringResult = await awaitify(foo, "hello", null, null); // string类型 const numberResult = await awaitify(foo, null, 123, null); // number类型
可以看到,TypeScript现在能准确根据传入的参数,推断出awaitify返回的Promise的具体类型了。
为什么原来的实现不行?
你原来的代码用了ReturnType<F>,但F是像foo这样的函数——它的返回值是void(因为这类回调函数本身不返回值,结果通过回调传递),所以Promise<ReturnType<F>>就变成了Promise<void>,完全不符合我们的预期。这就是类型推断失败的核心原因。
内容的提问来源于stack exchange,提问作者kchang4

