TypeScript:如何为异步函数组合编写 asyncPipe 函数?

J. *_*ers 5 asynchronous functional-programming function-composition promise typescript

我最近又在探索 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.'
Run Code Online (Sandbox Code Playgroud)

这里asyncPipe以反数学的顺序(从左到右)组合了常规函数和 promise。我很想asyncPipe用 TypeScript写一个,它知道输入和输出类型。所以userHasEvenName应该知道,它接受一个数字并返回一个Promise<boolean>. 或者,如果您注释掉getUserById并且asyncIsEven它应该知道它接受 aUser并返回一个数字。

以下是 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);
Run Code Online (Sandbox Code Playgroud)

我很想向你展示我所有的方法,asyncPipe但大多数方法都没有。我发现为了compose在 TypeScript 中编写函数,你必须重载它,因为 TypeScript 无法处理向后推理并按compose数学顺序运行。既然asyncPipe是从左到右写的,感觉写起来还是可以的。我能够明确地编写一个pipe2可以组合两个常规函数的函数:

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

您将如何编写asyncPipe异步组合任意数量的函数或承诺并正确推断返回类型的代码?

for*_*d04 6

变体 1:简单asyncPipe游乐场):

type MaybePromise<T> = Promise<T> | T

function asyncPipe<A, B>(ab: (a: A) => MaybePromise<B>): (a: MaybePromise<A>) => Promise<B>
function asyncPipe<A, B, C>(ab: (a: A) => MaybePromise<B>, bc: (b: B) => MaybePromise<C>): (a: MaybePromise<A>) => Promise<C>
// extend to a reasonable amount of arguments

function asyncPipe(...fns: Function[]) {
    return (x: any) => fns.reduce(async (y, fn) => fn(await y), x)
}
Run Code Online (Sandbox Code Playgroud)

例子:

const userHasEvenName = asyncPipe(getUserById, getName, countLetters, asyncIsEven);
// returns (a: MaybePromise<number>) => Promise<boolean>
Run Code Online (Sandbox Code Playgroud)

警告:即使所有函数参数都是同步的,这也将始终返回一个承诺。


变体 2:混合asyncPipe游乐场

让我们尝试使结果 a Promise,如果任何函数是异步的,否则返回同步结果。类型在这里很快变得臃肿,所以我只使用了一个带有一个重载(两个函数参数)的版本。

function asyncPipe<A, B, C>(ab: (a: A) => B, bc: (b: Sync<B>) => C): < D extends A | Promise<A>>(a: D) => RelayPromise<B, C, D, C>
// extend to a reasonable amount of arguments

function asyncPipe(...fns: Function[]) {
    return (x: any) => fns.reduce((y, fn) => {
        return y instanceof Promise ? y.then(yr => fn(yr)) : fn(y)
    }, x)
}
Run Code Online (Sandbox Code Playgroud)

我定义了两个助手:Sync将始终为您提供已解析的Promise 类型,RelayPromise将最后一个类型参数转换为Promise ,如果任何其他参数是Promise (有关更多信息,请参阅操场)。

例子:

const t2 = asyncPipe(getName, countLetters)(Promise.resolve({ displayName: "kldjaf" }))
// t2: Promise<number>

const t3 = asyncPipe(getName, countLetters)({ displayName: "kldjaf" })
// t3: number
Run Code Online (Sandbox Code Playgroud)

警告:如果您希望在一种类型中同时使用同步和异步,它会变得非常复杂,您应该对其进行广泛的测试(我的示例中可能还有一些,到目前为止我只使用了简单版本)。

也可能有一个兼容性原因,为什么fp-ts使用特殊版本的pipe,这可以更好地使用 TypeScript 的从左到右类型参数推断(这也可能是您的一个考虑因素)。


笔记

最后,您应该决定是否值得asyncPipe为 Promises提供一个特殊版本 - 更多类型和实现意味着更多潜在错误。

作为替代方案,pipe在函数式编程风格中使用带有函子或 monad的 simple 。例如,您可以切换到 aTaskTaskEither类型(参见 fp-ts 作为示例),而不是使用承诺。