TS 泛型的小试牛刀,踩坑踩坑

2022-05-11

前段时间我应该提到过,我在公司项目上参考 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 我还是得从头到尾,重新认识一下才行了==

配乐 雷雨 一般
概览页 时间轴
奇趣音乐盒 技术源于 Kico Player
Emmm,这里是歌词君