TypeScript:如何为异步函数组合编写 asyncPipe 函数?
TypeScript: How to write an asyncPipe function for asynchronous function composition?
我最近又在探索 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
以反数学顺序(从左到右)组成常规函数和 promises。我很想用 TypeScript 编写一个 asyncPipe
,它知道输入和输出类型。所以 userHasEvenName
应该知道,它接受一个数字并且 return 是一个 Promise<boolean>
。或者,如果您注释掉 getUserById
和 asyncIsEven
,它应该知道它接受一个 User
和 return 一个数字。
以下是 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
来异步组合任意数量的函数或 promises 并正确推断 return 类型?
变体 1:简单 asyncPipe
(playground):
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>
警告:这总是 return 承诺,即使所有函数参数都是同步的。
变体 2:混合 asyncPipe
(playground)
让我们尝试使结果成为 Promise
,如果任何函数是异步的,否则 return 是同步结果。类型在这里很快就会变得臃肿,所以我只使用了一个带有一个重载(两个函数参数)的版本。
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,如果任何其他参数都是一个承诺(有关更多信息,请参见游乐场)。
示例:
const t2 = asyncPipe(getName, countLetters)(Promise.resolve({ displayName: "kldjaf" }))
// t2: Promise<number>
const t3 = asyncPipe(getName, countLetters)({ displayName: "kldjaf" })
// t3: number
警告:如果你想在一种类型中同时使用同步和异步,它会变得非常复杂,你应该对其进行广泛的测试(我的样本中可能还有一些,到目前为止我只使用了简单的版本)。
也可能有兼容性原因,为什么 fp-ts uses a special version of pipe
可以更好地使用 TypeScript 的从左到右类型参数推断(这也可能是您的考虑因素)。
备注
最后,您应该决定是否值得为 Promises 专门开发一个特殊的 asyncPipe
版本——更多的类型和实现意味着更多的潜在错误。
作为替代方案,在函数式编程风格中使用带有仿函数或单子的简单 pipe
。例如。您可以切换到 Task
或 TaskEither
类型(以 fp-ts 为例),而不是使用 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
以反数学顺序(从左到右)组成常规函数和 promises。我很想用 TypeScript 编写一个 asyncPipe
,它知道输入和输出类型。所以 userHasEvenName
应该知道,它接受一个数字并且 return 是一个 Promise<boolean>
。或者,如果您注释掉 getUserById
和 asyncIsEven
,它应该知道它接受一个 User
和 return 一个数字。
以下是 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
来异步组合任意数量的函数或 promises 并正确推断 return 类型?
变体 1:简单 asyncPipe
(playground):
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>
警告:这总是 return 承诺,即使所有函数参数都是同步的。
变体 2:混合 asyncPipe
(playground)
让我们尝试使结果成为 Promise
,如果任何函数是异步的,否则 return 是同步结果。类型在这里很快就会变得臃肿,所以我只使用了一个带有一个重载(两个函数参数)的版本。
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,如果任何其他参数都是一个承诺(有关更多信息,请参见游乐场)。
示例:
const t2 = asyncPipe(getName, countLetters)(Promise.resolve({ displayName: "kldjaf" }))
// t2: Promise<number>
const t3 = asyncPipe(getName, countLetters)({ displayName: "kldjaf" })
// t3: number
警告:如果你想在一种类型中同时使用同步和异步,它会变得非常复杂,你应该对其进行广泛的测试(我的样本中可能还有一些,到目前为止我只使用了简单的版本)。
也可能有兼容性原因,为什么 fp-ts uses a special version of pipe
可以更好地使用 TypeScript 的从左到右类型参数推断(这也可能是您的考虑因素)。
备注
最后,您应该决定是否值得为 Promises 专门开发一个特殊的 asyncPipe
版本——更多的类型和实现意味着更多的潜在错误。
作为替代方案,在函数式编程风格中使用带有仿函数或单子的简单 pipe
。例如。您可以切换到 Task
或 TaskEither
类型(以 fp-ts 为例),而不是使用 promise。