考虑以下片段:
type Add = (a: number | string, b: number | string) => number | string;
function hof(cb: Add) {}
const addNum = (a: number, b: number): number => a + b;
const addStr = (a: string, b: string): string => a + b;
// @ts-expect-error
hof(addNum);
// @ts-expect-error
hof(addStr);
为什么我们不能将addNum
和addStr
函数传递给hof
,海事组织他们的呼叫签名应该与hof
所期望的兼容,但实际上并非如此。
如果它们的类型是不兼容的,那么为什么下面的代码片段中的重载签名没有抱怨呢?
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: number | string, b: number | string): number | string {
return 1;
}
发布于 2022-05-25 06:43:15
您假设是协变类型,但函数参数是相反的。(有关这方面的一些内容,请参见这篇文章。)
将您的示例稍微更改一下,您就会发现问题所在:
type Add = (a: number | string, b: number | string) => number | string;
function hof(cb: Add) {
return cb
}
const addNum = (a: number, b: number): number => Math.round(a) + Math.round(b);
const addStr = (a: string, b: string): string => `${a.toLowerCase()}${b.toLowerCase}`;
hof(addNum)('a', 'b'); // should error because addNum can't take strings
hof(addStr)(123, 456); // should error because addStr can't take numbers
这里,hof
返回传递它的回调函数。因此,如果您传递它addNum
,那么字符串就会崩溃。如果你把它传给addStr
,那么数字就会崩溃。
因此,若要将函数分配给函数类型,则必须接受所有参数类型的超集,而不是子集。
但是,如果将hof
设置为泛型,则可以根据参数创建函数类型。例如:
type Add<T extends number | string> = (a: T, b: T) => T;
function hof<T extends number | string>(cb: Add<T>): Add<T> {
return cb
}
const addNum = (a: number, b: number): number => Math.round(a) + Math.round(b);
const addStr = (a: string, b: string): string => `${a.toLowerCase()}${b.toLowerCase}`;
hof(addNum)(123, 456); // fine
hof(addStr)('a', 'B'); // fine
hof(addNum)('a', 'b'); // should error because addNum can't take strings
hof(addStr)(123, 456); // should error because addStr can't take numbers
发布于 2022-05-25 08:48:33
要了解这里发生了什么,我们需要了解hof
函数允许使用传递给它的cb
的方式。
什么论据可以通过?
让我们考虑一个简单的例子,只包含一个参数。
function hof(cb: (a: number | string) => number | string) {}
因此,hof
表示它需要一个回调函数cb
,它接受一个可以是数字或字符串的参数,并返回一个字符串或一个数字。
现在,考虑一下TS在hof
上可以传递给cb
的限制。因此,hof
可以使用number
或string
调用cb
,这是可以的,因为hof
已经声明它接收到的cb
应该能够同时处理数字和字符串。
但是,如果将以下double
函数传递给hof
,TS会发出抱怨:
function double(a: number): number {
return 2 * a
}
这是有意义的,因为hof
可以用string
调用这个double
函数,这可能会破坏double
函数,因为它只接受数字。
还请注意抛出错误的位置,它是在用double
调用double
的地方抛出的,这表明hof
的调用方式有问题,而不是如何实现它。
function hof(cb: (a: number | string) => number | string) {}
function double(a: number): number {
return 2 * a
}
// @ts-expect-error
hof(double)
因此,传递给hof
hof
的回调应该能够处理允许hof
传递给回调的一组参数。
如何使用返回类型?
现在,让我们看看hof
如何使用cb
返回的值。
因为cb
可以返回一个number
或一个string
,所以hof
有责任明智地使用返回值,这意味着hof
不应该假设返回的值总是数字或字符串,hof
需要在做出任何这样的假设之前设置适当的类型保护。
function hof(cb: (a: number | string) => number | string) {
// @ts-expect-error
const fixed = cb(100).toFixed() // wrong assumption that the returned value is always a number
}
function hof(cb: (a: number | string) => number | string) {
const ret = cb(100)
if (typeof ret === "number") { // appropriate check to make sure that it's a number
const fixed = ret.toFixed();
}
}
再次注意抛出错误的位置,它是在函数体中抛出的,这表明函数的实现存在问题。
传递给hof
的回调也可以返回一个number
,TS对此绝对没有问题。
function hof(cb: (a: number | string) => number | string) {}
function double(a: number | string): number {
return 2 * Number(a)
}
hof(double)
因此,传递给hof
hof
的回调应该只返回 only 返回hof
期望回调返回的一个子集。
过载签名
重载签名的行为是不同的,重载的目的是限制函数的行为。
因此,重载签名的参数和返回值应该始终是实现签名的参数和返回值的子集。
发布于 2022-05-25 06:42:51
TL;DR: hof
需要回调来覆盖比这两个函数单独处理的更多的情况。
在这个案子里你搞错了。
hof
指定将用两个参数调用其回调,其中一个参数可以是字符串,也可以是数字。这意味着一个只能处理两个数字参数的函数是不够的(因为它也可能被传递一个字符串)。
所以,另一种方法会起作用:
function hof(cb: (a: number, b: number) => number) {}
function add(a: number | string, b: number | string): number | string {
return 1;
}
hof(add);
因为在这种情况下,add保证能够处理传递给它的两个数字(至少基于它的签名)。
那么为什么重载签名会起作用呢?因为上下文是完全不同的。作为函数的重载,它们是完全正确的,因为它们是更一般函数的更具体的签名。但这是函数参数的错误方向,您需要传递一个参数与函数或较小的所要求的参数相同的函数。您传递了一个具有更具体参数的函数。
https://stackoverflow.com/questions/72372766
复制相似问题