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

Type inference of Function composition method (chain) in TypeScript

我尝试实现一个函数,该函数扩展指定函数以具有 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是对指定函数进行扩展,使其具有函数组合方法的函数。

我设法让它以某种方式工作;但是我发现至少有两个问题。

1.

现在,我们用F代替f2F2的类型是((a: number) => string) & F,这是可以预料的。

然后,我们用F代替f1,与f2合成,F12的类型也是((a: number) => string) & F,即预期。

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

现在,(F2).compose(identity) 的类型是 ((a: number) => string) & F,这是可以预期的。

然而,(F12).compose(identity) 的类型是 ((a: number) => number) & F,这是不可预期的。

我跟踪了很久我的代码,但我不知道为什么会这样。

你能给我建议吗?谢谢!

编辑:

请注意函数不应该包装在对象中,我的意图是提供一个直接给函数的组合方法:

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

  //not
  {
    f: f,
      compose:someFunction
  }

编辑:对于第 2 期,根据@jcalz 的评论,我创建了单独的问题:

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

2.

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

如果您检查 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 的函数对于某些 U 来说是 (a: T) => U 类型,returned 函数有一个 compose 方法接受一个回调,其第一个参数是输入 U。这很好,但是当你调用它的 compose 方法时,你的新事物的 compose 方法与旧事物的类型相同。它需要一个参数类型为 U 的回调。但是你想要一个参数类型为 V 的回调,之前传递给 compose() 的回调的 return 类型。这个问题永远存在:每次调用 compose() 都会 return 另一个东西 compose() 期望回调接受类型 U 而不是 return 类型的参数上次调用 compose().

所以这意味着您的 F(f1) return 是以下类型:

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() 编辑的类型 return 来跟踪来自原始回调参数的 T 类型和“当前”U类型,即最近传入参数的return类型。所以你定义的 F 类型应该是 U 中的 generic 才能表示这个。让我们将其从函数实现中提取出来,并将 F<T, U> 描述为调用 F(f) 的结果,其中 f 的类型为 (a: T) => U:

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

所以它有一个 call signature 接受类型 T 和 returns U 的值。它还有一个通用的 compose 方法,该方法为通用 V 和 return 一个 F<T, V> 接受类型 (b: U) => V 的回调。正是这种 VU 的替换让链跟踪最近的回调 return 类型。

实现如下:

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<number, string> 类型,所以当你调用它时,它会 return a string.


Playground link to code