前段时间我应该提到过,我在公司项目上参考 AHooks 写了个 useRequest
Hooks,今天打算继续完善下它的 TS 类型定义,想让它支持自动识别传入的参数,并原封不动的提供到其他方法(onSucceed / onFailed 等)上。
想到这个参数可能为空,我还错误的用了重载去写,实际上并不需要。最后用到了 Rest 参数这个特性(才了解到,真的菜),一般来说这东西的类型是 string[]
。
花了晚上将近 2 小时的时间研究,陆陆续续解决好了应用代码里面的报错,终于缩小了 Debug 范围,发现是 Hooks 返回对象里面的类型不对,最后改成了这样。这样写确实是正确的,但我还发现了更奇怪的问题,可选参数竟然变成了必须项?为了方便说明,下面这几段代码都是简化后的例子:
interface Result<T extends any[]> {
send: (...args: T) => void
}
function Request<T extends any[]>(api: (...args: T) => void, onSucceed?: (data: string, ...params: T) => void): Result<T> {
return {
send: (...args) => {
onSucceed && onSucceed("test", ...args);
}
}
}
// 注意看 (str, id) 这个位置
const a = Request((id?: number) => `id: ${id}`, (str, id) => console.log("args", str, id));
// ⚠️ 异常:应有 1 个参数,但获得 0 个,未提供 rest 形参“args”的实参
a.send();
// ✅ 正常
a.send(14);
结合上面的 Result,根据这个类型定义,api
函数传入多少个参数,send
方法也就会传回来多少个参数。奇怪的地方就是,如果有一个参数设置成了可选,send
方法会提示不能为空。我就纳闷了,为什么它被自动转换成了必须项呢?
起初我尝试用 void
代替可选项(id?
变成了 id: number | void
),这确实可以,但又可能导致onSucceed
里面需要额外判断 id
是否不存在。假如它可能是对象,就无法使用 params ?. type
这种链式判断符了。
最后发现,这其实是因为除了 api
函数,onSucceed
函数里面没有将参数 id
设置成可选项,才使得 id
“莫名其妙”变成了必填,最终send
方法报错。在试错期间,我还意外发现了下面这段代码的写法不会报错,为什么呢?
function RequestB<T extends any[]>(api: (...args: T) => void, onSucceed?: (...args: T) => void): Result<T> {
return {
send: (...args) => {
onSucceed && onSucceed(...args);
}
}
}
// 这里的 id? 没有指定类型
const b = RequestB((id?) => "id", (id) => console.log(id));
b.send();
b.send(123);
相对于第一个例子,这里的 id?
并没有指定类型,默认是 any
,而 any
本身是不会参与 TS 校验的,所以给了我“它能工作”的假象,一旦加上类型,就死翘翘。
虽然说这一个鬼东西看了这么久,也绕了不少弯子,显得我有些愚蠢(@Innei:这还不是体操,就是一个简单的泛型),可这么认真的去琢磨之后,的确就更了解具体的原因了,感觉 TS 我还是得从头到尾,重新认识一下才行了==