TypeScript中函数组合方法(链)的类型推断

Posted

技术标签:

【中文标题】TypeScript中函数组合方法(链)的类型推断【英文标题】:Type inference of Function composition method (chain) in TypeScript 【发布时间】:2021-12-15 20:27:48 【问题描述】:

我尝试实现一个函数,该函数将扩展指定函数以具有 function composition 的可链接方法;如下;

另见: TypeScript playground


  const F = <T, U>(f: (a: T) => U) => 
    type F = 
      compose: <V, >(g: (b: U) => V) => (((a: T) => V) & F);
    ;
    return Object.defineProperty(f, "compose", 
      configurable: true,
      value: <V,>(g: (b: U) => V) => F((a: T) => g(f(a)))
    ) as ((a: T) => U) & F
  ;
  //--------------------------------
  const f1 = (a: number) => a + 1;
  const f2 = (a: number) => a.toString();
  const identity = <T,>(a: T) => a;
  //--------------------------------
  const F2 = F(f2);
  // ((a: number) => string) & F  // good
  const F12 = F(f1).compose(f2);
  // ((a: number) => string) & F  // good
  //--------------------------------
  const F2i = (F2).compose(identity);
  // ((a: number) => string) & F  // good
  const f12i = (F12).compose(identity);
  // ((a: number) => number) & F  // bad  why??
  //--------------------------------
  const fi1 = F(identity).compose(f1);
  /* ts(2345) error
    Argument of type '(a: number) => number' is not assignable to parameter of type '(b: unknown) => number'.
        Types of parameters 'a' and 'b' are incompatible.
          Type 'unknown' is not assignable to type 'number'.
    const f1: (a: number) => number
  */

在这个示例代码中,我们有 3 个基本功能; f1f2identity

F是一个函数,就是将一个指定的函数扩展成一个函数组合的方法。

我设法让它工作了;但是我发现至少有 2 个问题。

1.

现在,我们将F 用于f2,而F2 的类型是((a: number) =&gt; string) &amp; F,这是意料之中的。

那么,我们用F代替f1,用f2组合,F12的类型也是((a: number) =&gt; string) &amp; F,这是意料之中的。

因此,F2F12 的类型是相同的,到目前为止,很好。

现在,(F2).compose(identity) 的类型应该是 ((a: number) =&gt; string) &amp; F

但是,(F12).compose(identity) 的类型是 ((a: number) =&gt; number) &amp; F,这是意料之中的。

我已经跟踪我的代码很长时间了,但我不知道为什么会发生这种情况。

你能给我建议吗?谢谢!

编辑:

请注意函数不应该被包裹在Object中,我的意图是直接为函数提供一个compose方法

 const f = (a: number) => a + 1;
  const fg = f.compose(g);

  //not
  
    f: f,
      compose:someFunction
  

编辑:对于第二个问题,@jcalz 的 cmets,我创建了单独的问题:

Is there any workaround for ts(2345) error for TypeScript lacks higher kinded types?

2.

如图所示,我有 ts(2345) 错误,错误消息对我来说没有意义,所以我不知道如何解决这个问题。

【问题讨论】:

我意识到问题是关于推理的,但是显式传递泛型有什么问题? const fi1 = F&lt;number, number&gt;(identity).compose(f1); 类型检查就好了.... 您的两个问题应该是两个独立的问题。您的第一个问题是您定义Fcompose() 方法以返回与自身相同的类型F,这意味着链中的每个后续链接仍将期望回调采用原始U 类型。如果你想正确地进行链接,你需要改用this 之类的东西。 您的第二个问题是 TypeScript 缺少表达您想要做的事情所需的更高种类的类型;尽管有一些 support 用于类似的操作,但您不能在此处使用返回的通用函数来执行此操作,该函数还具有其他属性。所以这是不可能的,你需要手动指定参数。 问题是:这两件事中的哪一件是您真正的问题?我很乐意回答其中一个问题,但将两者联系在一起的唯一原因是您使用相同的代码遇到了这两个问题。如果你对一件事提出这个问题,我会用一个解释来回答它。您应该随时就另一件事提出单独的问题。 @jcalz 感谢您每次的输入,您总是大大改进了我的代码!真的很感激..那么,很明显,你已经在你的第一条评论中用操场代码解决了我的#1问题,所以我想要求回答#2的问题。谢谢! 【参考方案1】:

如果您检查 const F 的类型(值,而不是私人命名的类型)并将其展开一点,您遇到的问题应该很明显:

const alsoF: <T, U>(f: (a: T) => U) =>
  ((a: T) => U) & 
    compose: <V>(g: (b: U) => V) => (((a: T) => V) & 
      compose: <V>(g: (b: U) => V) => (((a: T) => V) & 
        compose: <V>(g: (b: U) => V) => (((a: T) => V) & 
          compose: <V>(g: (b: U) => V) => (((a: T) => V) & 
            compose: <V>(g: (b: U) => V) => (((a: T) => V) & any);
          );
        );
      );
    );
   = F; // okay

(在深入了几层之后,我用any 退出了,但是如果您创建新的命名类型,则不需要这样做;不过,这对于本次讨论无关紧要。)如果您传递的函数到F 的类型为(a: T) =&gt; U 对于某些U,返回的函数有一个compose 方法接受一个回调,其第一个参数是U 类型。没关系,但是当您调用 compose 方法时,您的新事物的 compose 方法与旧方法的类型相同。它需要一个参数为U 类型的回调。但是您想要一个回调,其参数类型为V,即之前传递给compose() 的回调的返回类型。而且这个问题永远存在:每次调用compose() 都会返回另一个东西,它的compose() 期望回调接受U 类型的参数,而不是之前调用compose() 的返回类型。

这意味着您的F(f1) 返回以下类型:

const fF1 = alsoF(f1);
/* const alsoF12: ((a: number) => string) & 
compose: <V>(g: (b: number) => V) => ((a: number) => V) & 
    compose: <V>(g: (b: number) => V) => ((a: number) => V) & 
        ...;
    ;
;  */

因此,您的F12 属于以下类型:

const alsoF12 = alsoF(f1).compose(f2);
/* const alsoF12: ((a: number) => string) & 
compose: <V>(g: (b: number) => V) => ((a: number) => V) & 
    compose: <V>(g: (b: number) => V) => ((a: number) => V) &  ... ;
;   */

哎呀,它总是希望对compose() 的回调采用number,而不是您想要的string。所以你的f12i 是这样的:

const alsoF12i = (alsoF12).compose(identity);
/* const alsoF12i: ((a: number) => number) & 
compose: <V>(g: (b: number) => V) => ((a: number) => V) & 
    compose: <V>(g: (b: number) => V) => ((a: number) => V) &  ...  ;
  ;  */

这不是你想要的。


要解决此问题,您将需要从 compose() 返回的类型来跟踪原始回调参数中的 T 类型和“当前”U 类型,这是最常见的返回类型最近传入的参数。所以你定义的F类型应该是U中的generic,以便表示这一点。让我们将其从函数实现中提取出来,并将F&lt;T, U&gt; 描述为调用F(f) 的结果,其中f 的类型为(a: T) =&gt; U

interface F<T, U> 
  (a: T): U;
  compose<V>(g: (b: U) => V): F<T, V>;

所以它有一个call signature,它接受T 类型的值并返回U。它还有一个通用的compose 方法,它为通用V 接收一个(b: U) =&gt; V 类型的回调,并返回一个F&lt;T, V&gt;。正是这种将V 替换为U 的方法让链能够跟踪最近的回调返回类型。

下面是实现:

const F = <T, U>(f: (a: T) => U): F<T, U> => 
  return Object.defineProperty(f, "compose", 
    configurable: true,
    value: <V,>(g: (b: U) => V) => F((a: T) => g(f(a)))
  ) as any;
;

让我们试试吧:

const f1 = (a: number) => a + 1;
const f2 = (a: number) => a.toString();
const identity = <T,>(a: T) => a;
//--------------------------------
const F2 = F(f2);
// const F2: F<number, string> 
const F12 = F(f1).compose(f2);
// const F12: F<number, string>
//--------------------------------
const F2i = (F2).compose(identity);
// const F2i: F<number, string>
const f12i = (F12).compose(identity);
// const f12i: F<number, string>

console.log(f12i(123).repeat(2)) // "124124"

看起来不错。编译器现在知道f12iF&lt;number, string&gt; 类型,所以当你调用它时,它会返回一个string


Playground link to code

【讨论】:

非常感谢!非常抱歉我的回复延迟了。我会研究你的答案。 再次,您的回答非常详尽且内容丰富,我研究了您的代码并非常感谢。 在处理完您详细说明的答案后,当我尝试在interface F&lt;T, U&gt; 中注释掉(a: T): U; 时,发现一些复杂代码的类型检查失败,所以我知道这真的很有必要。但是我不知道它为什么以及如何工作,因为 a 没有从任何地方引用。你能给我建议吗?谢谢! 是call signature;如果您希望 F&lt;T, U&gt; 像函数一样可调用,那么您需要一个调用签名。当你说a 没有被提及时,那似乎是***.com/questions/60114432/… 可能吗?无论如何,它超出了原始问题的范围,因此如果您还有其他问题,您可能想要创建一个新帖子。我不一定承诺单独在 cmets 上做太多后续工作(尤其是由于角色 lim 谢谢!!再次。所以现在我发现当我们将 javascript 函数视为对象时,我们需要将调用签名属性键入为默认值。当我们有一个函数时:((a: A) =&gt; B) 有一个属性, (a: A): B 应该是基数?

以上是关于TypeScript中函数组合方法(链)的类型推断的主要内容,如果未能解决你的问题,请参考以下文章

typescript 从子类调用的构造函数参数推断类型

TypeScript 中对象字面量的动态泛型类型推断

TypeScript 参数类型推断失败

在 TypeScript 中推断泛型函数类型

TypeScript 无法在扩展语法函数调用中推断数组类型

TypeScript 类型推断 - 函数的通用对象