如何使用打字稿创建EventHandler类?

问题描述 投票:0回答:3

我正在尝试将我的项目从 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();
}

现在我不知道如何在打字稿中编写此代码而不丢失有关事件输入的信息。例如:

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
这里的事件类型吗?

typescript generics
3个回答
2
投票

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之前,监听器的参数可能不会被推断为正确的类型(它们可能被推断为

any
)。如果您遇到这个问题,请告诉我,我可以提供 3.0 之前的版本。


0
投票

@titian-cernicova-dragomir 我支持这样的事情(不起作用):

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
提供约束。


0
投票

我知道这有点晚了,但我发现自己陷入了这个问题,所以我将把我发现非常有用的内容留在这里。 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

© www.soinside.com 2019 - 2024. All rights reserved.