TypeScript 函数组合方法(链)的类型推断
Type inference of Function composition method (chain) in TypeScript
我尝试实现一个函数,该函数扩展指定函数以具有 function composition 的可链接方法;如下;
{
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个基本函数; f1
、f2
和 identity
。
F
是对指定函数进行扩展,使其具有函数组合方法的函数。
我设法让它以某种方式工作;但是我发现至少有两个问题。
1.
现在,我们用F
代替f2
,F2
的类型是((a: number) => string) & F
,这是可以预料的。
然后,我们用F
代替f1
,与f2
合成,F12
的类型也是((a: number) => string) & F
,即预期。
因此,F2
和 F12
的类型是相同的,到目前为止,很好。
现在,(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
的回调。正是这种 V
对 U
的替换让链跟踪最近的回调 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"
看起来不错。编译器现在知道 f12i
是 F<number, string>
类型,所以当你调用它时,它会 return a string
.
我尝试实现一个函数,该函数扩展指定函数以具有 function composition 的可链接方法;如下;
{
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个基本函数; f1
、f2
和 identity
。
F
是对指定函数进行扩展,使其具有函数组合方法的函数。
我设法让它以某种方式工作;但是我发现至少有两个问题。
1.
现在,我们用F
代替f2
,F2
的类型是((a: number) => string) & F
,这是可以预料的。
然后,我们用F
代替f1
,与f2
合成,F12
的类型也是((a: number) => string) & F
,即预期。
因此,F2
和 F12
的类型是相同的,到目前为止,很好。
现在,(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
的回调。正是这种 V
对 U
的替换让链跟踪最近的回调 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"
看起来不错。编译器现在知道 f12i
是 F<number, string>
类型,所以当你调用它时,它会 return a string
.