您的功能可能很简单(无论如何这是值得商榷的),但 generic 打字绝非如此。您正在尝试表示任意长度类型的“链”。本质上,您从 I 类型的初始值开始,然后对于某些输出类型 TFirst,可能是 (input: Awaited<I>) => Promise<Awaited<TFirst>> 类型的函数,然后可能是 (input: Awaited<TFirst>) => Promise<Awaited<TSecond>> 等类型的函数,等等。 ,最后以 (input: Awaited<TPenultimate>) => Promise<Awaited<TLast>> 类型的函数结束,然后 pipe() 的输出是 Promise<Awaited<TLast>> 类型的值,除非没有函数而只有输入 I,在这种情况下输出是I。
带有the Awaited type的部分正在处理这样一个事实,即如果你await一个非承诺值,你就会得到这个值,所以Awaited<string>是string,Awaited<Promise<string>>是string......你可以真的嵌套承诺,所以 Awaited<Promise<Promise<string>>> 也是 string。
因此,pipe() 的一种方法如下所示:
const pipe: <I, T extends any[]>(
init: I,
...fns: { [N in keyof T]: (input: Awaited<Idx<[I, ...T], N>>) => T[N] }
) => T extends [...infer _, infer R] ? Promise<Awaited<R>> : I =
(...args: any[]): any => args.reduce((prev, exec) => {
if (typeof exec !== 'function') {
return exec;
}
const getNextInPipe = async () => {
return exec(await prev);
};
const value = getNextInPipe();
return value;
});
type Idx<T, K> = K extends keyof T ? T[K] : never;
I类型参数对应init函数参数的类型。 T类型参数对应fnsrest参数中每个函数输出类型的tuple。所以如果有两个函数,第一个函数返回Promise<boolean>,第二个函数返回string,那么T就是[Promise<boolean>, string]。
fns 参数的类型是复杂性所在。对于fns的元素,在类数字索引N(认为第一个是0,第二个是1),我们知道输出类型是T的第N元素,或indexed access typeT[N]。这很简单。但是输入类型来自以前的T 的元素。或者I。我们首先制作 [I, ...T] 来表示,它使用 variadic tuple type 表示在 I 到 T 之前。然后我们只需要其中的Nth 元素。从概念上讲,这是索引访问 [I, ...T][N]。但是编译器不够聪明,无法意识到 T 元组类型的每个数字索引 N 也将是 [I, ...T] 元组类型的索引。所以我需要使用 Idx 帮助程序类型来说服编译器执行该索引。
至于输出类型,我们需要梳理 T 以找到它的最后一个元素 R(使用 conditional type inference)。如果存在,那么我们将返回一个 Promise<Awaited<R>> 类型的值。如果不是,那是因为T 为空,所以我们只返回I。
呼。
好的,让我们测试一下。首先支持的用途:
const z = pipe(3, (n: number) => n.toFixed(2), (s: string) => s.length === 4)
// const pipe: <3, [string, boolean]>(
// init: 3,
// fns_0: (input: 3) => string,
// fns_1: (input: string) => boolean
// ) => Promise<boolean>
// const z: Promise<boolean>
z.then(v => console.log("z is", v)) // z is true
const y = pipe(4);
// const pipe: <4, []>(init: 4) => 4
// const y: 4
console.log("y is", y) // y is 4
const x = pipe(50, (n: number) => new Promise<string>(
r => setTimeout(() => { r(n.toFixed(3)) }, 1000)),
(s: string) => s.length === 4);
// const pipe: <50, [Promise<string>, boolean]>(
// init: 50,
// fns_0: (input: 50) => Promise<string>,
// fns_1: (input: string) => boolean
// ) => Promise<boolean>
// const x: Promise<boolean>
x.then(v => console.log("x is", v)) // x is false
这一切看起来都不错。 z 和 x 是预期类型的承诺,而 y 只是一个数值。现在对于不受支持的情况:
pipe(); // error!
// Expected at least 1 arguments, but got 0.
pipe(10, 20, 30); // error!
// Argument of type 'number' is not assignable to parameter of type '(input: 10) => unknown'.
pipe(10, (x: string) => x.toUpperCase()) // error!
// Type 'number' is not assignable to type 'string'.
pipe(10, (x: number) => x.toFixed(2), (x: boolean) => x ? "y" : "n") // error!
// Type 'string' is not assignable to type 'boolean'
这些都因违反功能约束而失败。它至少需要一个参数,并且只有第一个参数可以是非函数。每个函数都需要接受前一个函数的等待响应(或初始值),如果不接受,则会出现错误。
所以这是我能做的最好的工作。它不是完美的;如果你看的话,我相信你能找到边缘案例。显而易见的是,如果您不使用 annotate 回调参数,那么推理可能会失败。 pipe(10, x => x.toFixed(), y => y.toFixed())之类的东西应该会产生错误但不会,因为编译器无法推断出x应该是number并且它回落到any,之后所有输入和输出都是any .如果你想让它被抓住,你需要写pipe(10, (x: number)=>x.toFixed(), (y: number)=>y.toFixed())。可能有一些调整可以改善这一点,但我不会再花时间在这里寻找它们。
要点是你可以代表这种东西,但它并不简单。
Playground link to code