泛型 - 我如何用 TypeScript 创建 EventHandler 类?
我正在尝试将我的项目从 JavaScript 迁移到 TypeScript,并且在迁移处理事件的类时遇到了问题。
为了避免对添加/移除事件监听器的选项进行重复描述,我们使用这样的包装器:
constructor() {
this.windowResizeHandler = new MyEventHandler(
target: window,
event: 'resize',
handler: e => this.handleResize_(e),
options: {passive: true, capturing: true},
);
}
connectedCallback() {
this.windowResizeHandler.add();
}
disconnectedCallback() {
this.windowResizeHandler.remove();
}
现在我不知道如何在 TypeScript 中编写这个而不丢失关于事件类型的信息。例如:
document.createElement('button').addEventListener('click', e => {
// Here e is MouseEvent.
});
但是如果我这样写我的包装器:
interface EventHandlerParams {
readonly target: EventTarget;
readonly event: Event;
readonly listener: (e: Event) => void;
readonly params: AddEventListenerOptions;
}
export class EventHandler {
public constructor(params: EventHandlerParams) {}
}
然后我丢失了类型定义:
new MyEventHandler(
target: document.createElement('button'),
event: 'click',
handler: e => { /* Here e is just Event not MouseEvent */ },
options: {passive: true, capturing: true},
);
我在这里有任何选项可以使用 lib.dom.d.ts 中的事件类型吗?
我尝试过类似这样的东西:
interface Mapping {
[Window]: WindowEventMap;
[HTMLElement]: HTMLElementEventMap;
}
interface EventHandlerParams<TTarget extends keyof Mapping,
TEventName extends keyof Mapping[TTarget],
TEvent extends Mapping[TTarget][TEventName]> {
readonly event: TEventName;
readonly listener: (event: TEvent) => void;
readonly params?: AddEventListenerOptions;
readonly target: TTarget;
}
export class EventHandler<TTarget extends keyof Mapping,
TEventName extends keyof Mapping[TTarget],
TEvent extends Mapping[TTarget][TEventName]> {
public constructor(params: EventHandlerParams<TTarget, TEventName, TEvent>) {}
}
但是我不能使用这个,因为类型不能作为接口属性,并且没有其他选项为 TTarget 提供约束。
答案#1
lib.dom.ts 中有一个类型,它包含所有事件名称与事件参数类型之间的映射。它被称为 WindowEventMap。
所以例如我们可以写如下内容:
interface EventHandlerParams<T extends keyof WindowEventMap> {
readonly target: EventTarget;
readonly event: T;
readonly options: AddEventListenerOptions;
readonly listener: (e: WindowEventMap[T]) => void
}
export class EventHandler<T extends keyof WindowEventMap> {
public constructor(params: EventHandlerParams<T>) { }
}
new EventHandler({
target: document.createElement('button'),
event: 'click',
options: { passive: true },
listener: e => { e.x /* e is MouseEvent */ }
});
EventHandlerParams 现在是泛型的,并且会将事件名称捕获为类型参数 T。我们还将 EventHandler 设为泛型,它的 T 将由传递给它的参数决定。通过获得 T(它将包含事件的字符串字面量类型),我们可以从 WindowEventMap 获取事件的实际参数类型,并在我们的监听器签名中使用它。
注意 我认为在 3.0 之前,传递给 listener 的参数可能无法推断为正确的类型(可能被推断为 any)。如果你遇到这个问题,请告诉我,我可以提供 3.0 之前的版本。
答案#2
我知道这有点晚,但我自己遇到了这个问题,所以我打算在这里留下我觉得非常有用的内容。James Garbutt 有一篇关于这个的好文章:在 TypeScript 中使用强类型事件。
简短的回答是:扩展全局的 EventMap 之一。TypeScript 有各种内置的映射,总结了常见的 DOM 事件。我将以 WindowEventMap 为例,但请记住还有其他映射,例如 DocumentEventMap 或 HTMLElementEventMap。
//This is something i used in a small game.
interface AudioEventDetail {
action: 'play' | 'pause' | 'stop' | 'resume';
type: 'background' | 'ui';
audioId: number;
}
declare global {
interface WindowEventMap {
'audioEvent': CustomEvent<AudioEventDetail>;
//You can add multiple definitions here:
'tooltipEvent': CustomEvent<TooltipDetail>; //Like this.
}
}
在这里,我只是扩展了接口 WindowEventMap,因为想法是添加一个像这样的监听器:
window.addEventListener('audioEvent', (event: CustomEvent<AudioEventDetail>) => {
const { action, type, audio } = event.detail;
//Here you have strongly typed access to action, type and audio parameters.
});
要发出它,你需要这样做:
window.dispatchEvent(new CustomEvent('audioEvent', { detail: { action: 'play', type: 'background', audio: 0 } }));
我唯一不喜欢这种方法的地方是,添加事件监听器时会是强类型的,但在构建它们时,基本上依赖于 CustomEvent 构造函数。
不过,您可以创建一个 class 来作为这种用途的语法糖:
class AudioEvent extends CustomEvent<AudioEventDetail> {
constructor(detail: AudioEventDetail) {
super('audioEvent', { detail });
}
}
//So when extending WindowEventMap it would look like this:
declare global {
interface WindowEventMap {
'audioEvent': AudioEvent; //Here we use the class instead.
}
}
//When Adding Listeners you're using the same class to type the event itself.
window.addEventListener('audioEvent', (event: AudioEvent) => {
const { action, type, audio } = event.detail;
//Your code here...
});
//And when emmiting a new event you'll use a constructor with strongly typed event parameters.
window.dispatchEvent(new AudioEvent({ action: 'play', type: 'background', audio: 0 }));
既然我们正在使用构造函数,你可以直接请求每个参数或设置默认值。你将再也不会丢失那些类型了!
希望能帮助到某人!:D
