【问题标题】:How to define a simple pipe function with generics?如何用泛型定义一个简单的管道函数?
【发布时间】:2023-01-08 01:18:03
【问题描述】:

我写了一个简单的管道函数,它接受异步函数,或者只接受传递而不执行的值。

我真的尝试过使用泛型来定义它,但并没有让它恢复到使用 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,是否可以做到?因为管道中每个函数的输出类型不依赖于输入类型。

我还是仿制药的新手,在此先感谢

【问题讨论】:

  • 您尝试支持哪些类型的呼叫?你想允许没有参数的pipe()吗(这是一个运行时错误)?你想允许pipe(3,4,5)(最终完全忽略34并返回5吗?)似乎唯一有用的调用是pipe(initValue, func0, func1, func2, ...),其中每个函数接受(等待)前一个输入的输出。
  • this approach 是否满足您的需求?如果是这样,我可以写一个答案来解释它;如果没有,我错过了什么?
  • @jcalz 哇,太棒了,非常感谢你花时间写这篇文章,你是完全正确的,唯一有用的调用是一个初始值,然后是函数。老实说,我几乎看不懂你写的语法,我会尝试更深入地学习这个,再次感谢!
  • @jcalz,如果可以问的话,你会如何推荐我学习泛型的高级用法?我确实有一个不错的 ts Udemy 课程,但它非常基础。也许是一本书,或者一个视频课程?
  • 我的评论不构成回答;当我有机会解释时,我会写一个完整的答案。我不确定要向其他人推荐什么来学习高级 TS;我花了很多时间阅读发行说明和 github 问题,但我认为这不会让任何人快速上手。

标签: javascript typescript


【解决方案1】:

您的功能可能很简单(无论如何这是值得商榷的),但 generic 打字绝非如此。您正在尝试表示任意长度类型的“链”。本质上,您从 I 类型的初始值开始,然后对于某些输出类型 TFirst,可能是 (input: Awaited&lt;I&gt;) =&gt; Promise&lt;Awaited&lt;TFirst&gt;&gt; 类型的函数,然后可能是 (input: Awaited&lt;TFirst&gt;) =&gt; Promise&lt;Awaited&lt;TSecond&gt;&gt; 等类型的函数,等等。 ,最后以 (input: Awaited&lt;TPenultimate&gt;) =&gt; Promise&lt;Awaited&lt;TLast&gt;&gt; 类型的函数结束,然后 pipe() 的输出是 Promise&lt;Awaited&lt;TLast&gt;&gt; 类型的值,除非没有函数而只有输入 I,在这种情况下输出是I

带有the Awaited type的部分正在处理这样一个事实,即如果你await一个非承诺值,你就会得到这个值,所以Awaited&lt;string&gt;stringAwaited&lt;Promise&lt;string&gt;&gt;string......你可以真的嵌套承诺,所以 Awaited&lt;Promise&lt;Promise&lt;string&gt;&gt;&gt; 也是 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&lt;boolean&gt;,第二个函数返回string,那么T就是[Promise&lt;boolean&gt;, string]

fns 参数的类型是复杂性所在。对于fns的元素,在类数字索引N(认为第一个是0,第二个是1),我们知道输出类型是T的第N元素,或indexed access typeT[N]。这很简单。但是输入类型来自以前的T 的元素。或者I。我们首先制作 [I, ...T] 来表示,它使用 variadic tuple type 表示在 IT 之前。然后我们只需要其中的Nth 元素。从概念上讲,这是索引访问 [I, ...T][N]。但是编译器不够聪明,无法意识到 T 元组类型的每个数字索引 N 也将是 [I, ...T] 元组类型的索引。所以我需要使用 Idx 帮助程序类型来说服编译器执行该索引。

至于输出类型,我们需要梳理 T 以找到它的最后一个元素 R(使用 conditional type inference)。如果存在,那么我们将返回一个 Promise&lt;Awaited&lt;R&gt;&gt; 类型的值。如果不是,那是因为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'

这些都因违反功能约束而失败。它至少需要一个参数,并且只有第一个参数可以是非函数。每个函数都需要接受前一个函数的等待响应(或初始值),如果不接受,则会出现错误。


所以这是我能做的最好的工作。它不是完美的;如果你看的话,我相信你能找到边缘案例。显而易见的是,如果您不使用 annotate 回调参数,那么推理可能会失败。 pipe(10, x =&gt; x.toFixed(), y =&gt; y.toFixed())之类的东西应该会产生错误但不会,因为编译器无法推断出x应该是number并且它回落到any,之后所有输入和输出都是any .如果你想让它被抓住,你需要写pipe(10, (x: number)=&gt;x.toFixed(), (y: number)=&gt;y.toFixed())。可能有一些调整可以改善这一点,但我不会再花时间在这里寻找它们。

要点是你可以代表这种东西,但它并不简单。

Playground link to code

【讨论】:

    猜你喜欢
    • 2021-07-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-27
    • 1970-01-01
    • 2023-04-03
    • 2018-12-08
    • 1970-01-01
    相关资源
    最近更新 更多