typescript 如何用泛型定义一个简单的管道函数?

9rygscc1  于 2023-01-10  发布在  TypeScript
关注(0)|答案(1)|浏览(91)

我编写了一个简单的管道函数,它接受异步函数,或者只接受传递而不执行的值。
我真的尝试过使用泛型来定义它,但没有让它恢复到使用unknown来代替。

export const pipe = (...args: Array<unknown>): Promise<unknown> | unknown =>
  args.reduce((prev, exec) => {
    if (typeof exec !== 'function') {
      return exec;
    }

    const getNextInPipe = async (): Promise<unknown> => {
      return exec(await prev);
    };

    const value = getNextInPipe();
    return value;
  });

我试着这样写:

export const pipe = <T,>(...args: Array<unknown>): unknown =>
  args.reduce((prev, exec) => {
    if (typeof exec !== 'function') {
      return exec;
    }

    const getNextInPipe = async (): Promise<T> => {
      return exec(await prev);
    };

    const value = getNextInPipe();
    return value;
  });

但是我不知道如何替换另一个unknown,是否可以做到?因为管道中每个函数的输出类型不依赖于输入类型。
我对仿制药还是新手,先谢了

xn1cxnb4

xn1cxnb41#

您的函数可能很简单(这是有争议的),但generic类型是什么都不是。你试图表示一个"链"的类型的任意长度。本质上你开始与类型I的初始值,然后可能是一个函数的类型像(input: Awaited<I>) => Promise<Awaited<TFirst>>的一些输出类型TFirst,然后可能是一个函数的类型像(input: Awaited<TFirst>) => Promise<Awaited<TSecond>>,等等,等等,最后以(input: Awaited<TPenultimate>) => Promise<Awaited<TLast>>类型的函数结束,然后pipe()的输出是Promise<Awaited<TLast>>类型的值,除非没有函数而只有输入I,在这种情况下输出是I
类型为Awaited的部分处理的是这样一个事实:如果你await一个非承诺值,你会得到这个值,所以Awaited<string>stringAwaited<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类型参数对应fns rest参数中每个函数的输出类型的元组,所以如果有两个函数,第一个函数返回Promise<boolean>,第二个函数返回string,那么T将是[Promise<boolean>, string]
fns参数的类型是复杂性所在的地方。对于fns的numericlike索引N处的元素(第一个为0,第二个为1),我们知道输出类型是T的第N个元素,或者是indexed access typeT[N],这很简单,但是输入类型来自T的 * previous * 元素,或者是I,我们首先用[I, ...T]来表示它,它使用一个可变元组类型来表示将I前置到T。然后我们只需要其中的第N个元素。概念上,这是索引访问[I, ...T][N]。但是编译器不是'我不够聪明,无法意识到T元组类型的每个数字索引N也将是[I, ...T]元组类型的索引,所以我需要使用Idx帮助器类型来说服编译器执行该索引。
至于输出类型,我们需要分解T来找到它的最后一个元素R(使用条件类型推理),如果它存在,那么我们返回类型为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

zx是预期类型的承诺,而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'

这些都是因为违反了函数的约束而失败的。它至少需要一个参数,并且只有第一个参数可以是非函数。每个函数都需要接受前一个函数等待的响应(或者初始值),如果不接受,就会得到一个错误。
所以这是我能做的最好的工作了。它并不完美;我确信如果你仔细看的话你会发现一些边缘情况,最明显的一个就是如果你不注解回调参数,那么推理可能会失败,比如pipe(10, x => x.toFixed(), y => y.toFixed())应该产生一个错误,但是没有,因为编译器无法推断x应该是一个number,它退回到any。之后所有的输入和输出都是any,如果你想让它被捕获,你需要写pipe(10, (x: number)=>x.toFixed(), (y: number)=>y.toFixed()),可能有一些调整可以改进这个,但是我不打算在这里花更多的时间去寻找它们。
主要的一点是,你可以表示这种东西,但它并不简单。
Playground代码链接

相关问题