TypeScript:我都传了 type 了,能不能给我自动推导出 data 类型啊?

本文可理解为是上文的进阶篇。在编程领域经常会用到事件通讯,这是一段喜闻乐见的代码:

1
2
3
4
5
const event = new XEvent()

event.emit('msg', 'hello', 'world')

event.on('msg', (arg1, arg2) => {})

如果你是一位类型敏感的 tsser,不知你有没有想过以下问题:

  1. 当我 emit 的时候,一旦输入了 eventName ,TypeScript 可否对 emit 出去的其他参数做类型检查 & 代码提示
  2. 当我 on 的时候,一旦输入了 eventName, TypeScript 可否对 callback 入参做自动类型推导
  3. eventName 本身也同样拥有类型检查 & 代码提示

好,以上 3 点需求看完如果你一点思路都没有,建议先看一遍本文开头提到的文章。本篇就不对细节做详细赘述了。

首先,我们一般不会完整实现一个通用的事件通讯器,而是通过继承某个通用的事件通讯器来实现自己业务的。如此就会有了确定的 eventName ,这其实也是实现以上 3 点的前提。那就先来定义一份所有的事件类型吧:

1
2
3
interface XEvent {
msg: ?
}

埋个伏笔,上面的 ? 应当是什么内容?


好的,思考了 3 秒钟,你应当意识到它一定和对应事件的 emit 参数及 on 的 callback 入参有关。那既然二者都是为了函数入参,且你将来希望得到 参数名 + 参数类型 的代码提示,在 TypeScript 里能做到的类型应该就只有函数签名了吧。那干脆直接就将其定义为 on 的 callback 函数签名吧:

1
2
3
4
interface XEventsMap {
msg: (arg1: string, arg2: string) => void
msg2: (arg: number) => void
}

有了主类型定义接下来就好办了,其实就是沿着上篇文章的思路继续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Events from 'events' // 你继承的那个通用事件通讯器

class XEvents extends Events {
on<T extends keyof XEventsMap>(eventName: T, listener: XEventsMap[T]): this {
return super.on(eventName, listener)
}

emit<T extends keyof XEventsMap>(
eventName: T,
...args: Parameters<XEventsMap[T]>
): boolean {
return super.emit(eventName, ...args)
}
}

export default XEvents

和上篇不同的是泛型 T 的位置,这个我也不知道具体的术语叫啥,反正就是你想让谁的泛型能够根据入参的类型推导出来就把泛型给谁。这里你要还是把 TXEvent,那就必须得显式地 XEvent<'msg'> 才可以让 onemit 的入参被推导成唯一结果,否则它们将是 XEvent 所有泛型下的联合类型。

如此,你在 emiton 的时候都能获得类型检查 & 代码提示了(在线体验)。


上篇还埋了一个坑:

现在我们来填上它(在线体验):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface XEventsMap {
msg: (arg1: string, arg2: string) => void
msg2: (arg: number) => void
}

class XEvents extends Events {
on(eventName: 'msg', listener: XEventsMap['msg']): this
on(eventName: 'msg2', listener: XEventsMap['msg2']): this
on(eventName: string, listener: (...args: any[]) => void): this {
return super.on(eventName, listener)
}

emit(eventName: 'msg', ...args: Parameters<XEventsMap['msg']>): boolean
emit(eventName: 'msg2', ...args: Parameters<XEventsMap['msg2']>): boolean
emit(eventName: string, ...args): boolean {
return super.emit(eventName, ...args)
}
}

从可读性上我认为是优于泛型法的,但总觉得不如泛型法简洁易拓展,比如某些场景下我就真的需要传一些「非安全」值的时候,泛型法可以:

1
xEvent.emit<any>('msg', 1, 2, 3, 4)

而重载法大概就只能:

1
2
const anyEvent = xEvents as any
anyEvent.emit('msg', 1, 2, 3, 4)

所以你更倾向于哪种呢?或者你还有其他方式的话欢迎探讨。


 评论