You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何编写支持类型推断的TypeScript异步函数组合asyncPipe?

如何实现支持类型推断的TypeScript异步函数组合工具asyncPipe?

我最近重新探索TypeScript的时候,发现它在函数组合的类型标注能力上有个不小的局限——尤其是像asyncPipe这种混合了普通函数和Promise的异步组合工具,很难让TypeScript自动正确推断出输入输出类型。先给大家看看我想要做类型标注的原始JavaScript代码:

const getUserById = id => new Promise((resolve, reject) => id === 1 ? resolve({ id, displayName: 'Jan' }) : reject('User not found.'));
const getName = ({ displayName }) => displayName;
const countLetters = str => str.length;
const asyncIsEven = n => Promise.resolve(n % 2 === 0);
const asyncPipe = (...fns) => x => fns.reduce(async (y, f) => f(await y), x);
const userHasEvenName = asyncPipe(getUserById, getName, countLetters, asyncIsEven);
userHasEvenName(1).then(console.log); // ↳ false
userHasEvenName(2).catch(console.log); // ↳ 'User not found.'

这里的asyncPipe是从左到右(非数学组合的反向顺序)来组合普通函数和Promise的。我希望写出一个TypeScript版本的asyncPipe,能自动识别输入输出类型:比如userHasEvenName应该明确知道它接收number类型的参数,返回Promise<boolean>;如果我注释掉getUserById和asyncIsEven,它应该能推断出接收User类型参数,返回number类型。

目前我已经把基础函数都加上了TypeScript类型标注:

interface User { id: number; displayName: string; }
const getUserById = (id: number) => new Promise<User>((resolve, reject) => id === 1 ? resolve({ id, displayName: 'Jan' }) : reject('User not found.'));
const getName = ({ displayName }: { displayName: string }) => displayName;
const countLetters = (str: string) => str.length;
const asyncIsEven = (n: number) => Promise.resolve(n % 2 === 0);

我试过很多asyncPipe的实现,但类型推断效果都不好。了解到TypeScript编写compose函数需要大量重载,因为TS无法处理反向推断,而且compose是数学顺序(从右到左)。但asyncPipe是从左到右组合,我觉得应该可行,目前已经写出了能组合两个普通函数的pipe2:

function pipe2<A, B, C>(f: (arg: A) => B, g: (arg: B) => C): (arg: A) => C {
  return x => g(f(x));
}

现在的问题是:如何编写可异步组合任意数量函数或Promise、且能正确推断返回类型的asyncPipe?


解决方案:利用递归类型与Awaited实现类型安全的asyncPipe

要实现支持任意数量函数的类型推断,我们需要借助TypeScript的递归条件类型和Awaited工具类型(用来解包Promise的类型),同时通过函数重载来处理不同数量的参数情况。

以下是完整的实现:

// 定义递归类型,计算函数组合后的输入输出类型
type AsyncPipeFn<T> = T extends [(arg: infer A) => infer B, ...infer Rest]
  ? Rest extends []
    ? (arg: A) => Promise<Awaited<B>>
    : (arg: A) => Promise<Awaited<ReturnType<AsyncPipeFn<[(arg: Awaited<B>) => ReturnType<Rest[0]>, ...Rest extends [any, ...infer R] ? R : []]>>>>
  : never;

// 函数重载:覆盖0个、1个、多个函数的场景
function asyncPipe(): <T>(x: T) => Promise<T>;
function asyncPipe<T, R>(fn: (arg: T) => R): (arg: T) => Promise<Awaited<R>>;
function asyncPipe<T, A, R>(fn1: (arg: T) => A, fn2: (arg: Awaited<A>) => R): (arg: T) => Promise<Awaited<R>>;
function asyncPipe<T, A, B, R>(fn1: (arg: T) => A, fn2: (arg: Awaited<A>) => B, fn3: (arg: Awaited<B>) => R): (arg: T) => Promise<Awaited<R>>;
// 支持更多函数的泛型兜底
function asyncPipe<T extends Array<(...args: any[]) => any>>(
  ...fns: T
): AsyncPipeFn<T> {
  return (x: any) => fns.reduce(async (acc, fn) => fn(await acc), Promise.resolve(x)) as any;
}

验证类型推断效果

现在用我们的asyncPipe来测试之前的场景:

// 自动推断出 userHasEvenName: (id: number) => Promise<boolean>
const userHasEvenName = asyncPipe(getUserById, getName, countLetters, asyncIsEven);
userHasEvenName(1).then(res => {
  // res的类型是boolean
  console.log(res); // false
});
userHasEvenName(2).catch(err => {
  // err的类型是string
  console.log(err); // 'User not found.'
});

// 测试去掉首尾函数的情况
const countUserNameLength = asyncPipe(getName, countLetters);
// 自动推断出 countUserNameLength: (arg: { displayName: string; }) => Promise<number>
countUserNameLength({ displayName: "Jan" }).then(len => {
  // len的类型是number
  console.log(len); // 3
});

实现说明

  1. 递归类型AsyncPipeFn:遍历函数数组,依次将前一个函数的输出(解包Promise后)作为后一个函数的输入,递归计算最终的函数类型。
  2. 函数重载:通过重载处理不同数量的函数参数,让TypeScript在调用时能更精准地推断类型;对于超过重载数量的函数,递归类型依然能正确计算。
  3. Awaited类型:自动解包Promise的类型,确保异步函数的输出能正确传递给下一个函数的输入。

这样实现的asyncPipe既支持普通函数,也支持返回Promise的异步函数,并且能全程自动推断输入输出类型,完全满足需求。

内容的提问来源于stack exchange,提问作者J. Hesters

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 15:02:29