TypeScript:如何编写用于异步函数组合的asyncPipe函数?

问题描述 投票:0回答:1

我最近又在探索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应该知道,它接受一个数字并返回一个Promise<boolean>。或者,如果您注释掉getUserByIdasyncIsEven,它应该知道它接受了User并返回数字。

这里是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函数,您必须对其进行heavily overload,因为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));
}

您将如何编写asyncPipe,以异步方式组合任意数量的函数或promise,并正确推断返回类型?

typescript asynchronous promise functional-programming function-composition
1个回答
0
投票

变量1:简单的asyncPipeplayground):

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)
}

示例:

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

注意:即使所有函数参数都已同步,也将始终返回promise。


变量2:混合asyncPipeplayground

如果任何一个函数都是异步的,请尝试使结果为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)
}

我定义了两个助手:Sync总是会为您提供resolved 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

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

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


注意

最后,您应该决定是否有一个专门用于Promises的特殊pipe版本-更多类型和实现意味着更多潜在的bug。

作为替代,可以将简单的asyncPipe与功能编程样式中的函子或单子一起使用。例如。您可以切换到pipeTask类型(而不是使用Promise)(例如,以fp-ts为例)。

© www.soinside.com 2019 - 2024. All rights reserved.