Typescript 事件侦听器 - 类型到道具映射
Typescript event listener - type to props mapping
export enum GameEventType {
EVENT_ONE = 'event-one',
EVENT_TWO = 'event-two',
EVENT_THREE = 'event-three'
}
export type GameEvent =
| { type: GameEventType.EVENT_ONE; parameter: string }
| { type: GameEventType.EVENT_TWO; anotherParam: SomeType }
| { type: GameEventType.EVENT_THREE; };
type GameEventListener = (event: GameEvent) => void;
export class GameObserver {
private listenerMap = new Map<GameEventType, GameEventListener[]>();
public on(eventType: GameEventType, listener: GameEventListener) {
const existing = this.listenerMap.get(eventType) ?? [];
existing.push(listener);
this.listenerMap.set(eventType, existing);
}
public off(eventType: GameEventType, listener: GameEventListener) {
let existing = this.listenerMap.get(eventType) ?? [];
if (existing.length) {
existing = existing.filter((l) => l !== listener);
this.listenerMap.set(eventType, existing);
}
}
public fireEvent(event: GameEvent) {
const listeners = this.listenerMap.get(event.type) ?? [];
listeners.forEach((l) => l(event));
}
}
考虑上面的事件侦听器 class。当你注册一个事件时,你提供你关心的事件类型和一个回调来接收事件对象。触发事件时,通过给 type
属性 正确的 GameEventType
枚举值,它将建议该事件对象的其他道具。不错
不太好的是,消费者仍然必须确保事件类型符合预期,才能访问对象中的其他道具。即使消费者已经注册接收 GameEventType.EVENT_ONE
的事件,并且即使它只会在触发该事件类型时接收事件,消费者仍然必须说 if (event.type === GameEventType.EVENT_ONE)
才能到达整个事件对象中的道具。
gameObserver.on(GameEventType.EVENT_ONE, (event: GameEvent) => {
// This won't work - cannot access other props of event object
console.log(event.parameter)
// Must do this
if (event.type === GameEventType.EVENT_ONE) {
// In order to see the other props linked to that enum value under type
console.log(event.parameter)
}
});
我真正喜欢的是:
- 当通过
on
方法注册一个事件监听器时,它会警告不要分配一个参数与事件属性不匹配的回调函数
- 触发事件时,它会根据事件类型知道事件对象道具,并在您输入时自动 completes/suggests 这些道具名称
- 消费时,回调只有事件的属性而不是类型(我们知道类型,我们注册了这个回调!)因此不必检查事件类型以获得道具
我以不同的方式处理了后一点,使用 any
,它在前两点上失败了(并且希望避免任何)。
如有任何关于此问题的建议,我们将不胜感激!感谢阅读。
至少,您需要在事件的类型 K
中创建 on()
和 off()
方法 generic,以便编译器可以表示第一个和第二个参数之间的联系。让我们把 GameEvent
变成 GameEvent<K>
:
export type GameEvent<K extends GameEventType> = Extract<
| { type: GameEventType.EVENT_ONE; parameter: string }
| { type: GameEventType.EVENT_TWO; anotherParam: SomeType }
| { type: GameEventType.EVENT_THREE; }, { type: K }>
这仅对 type
属性 为 K
的成员使用 the Extract<T, U>
utility type to filter the original union。因此 GameEvent<GameEventType.EVENT_TWO>
将具体为 {type: GameEvenetType.EVENT_TWO; anotherParam: SomeType}
。而GameEventListener
对应的变化是:
type GameEventListener<K extends GameEventType> =
(event: GameEvent<K>) => void;
现在我们可以在on()
和off()
中使用K
:
public on<K extends GameEventType>(eventType: K, listener: GameEventListener<K>) {
// same implementation
}
public off<K extends GameEventType>(eventType: K, listener: GameEventListener<K>) {
// same implementation
}
您可以在调用以下方法之一时验证其行为是否符合要求:
const gameObserver = new GameObserver()
gameObserver.on(GameEventType.EVENT_ONE, (event) => {
console.log(event.parameter.toUpperCase()) // okay
});
您确实需要稍微调整您的其他代码来处理这些泛型。最棘手的地方是你的 listenerMap
属性,它使用 a Map
object。不幸的是,TypeScript 的 Map<K, V>
类型给每个 属性 相同的类型 V
,无论您使用的是 K
中的哪个键。最少的工作量但最不安全的是给它一个像 Map<GameEventType, GameEventListener<any>[]>
这样的类型,而不用担心潜在的不匹配:
private listenerMap = new Map<GameEventType, GameEventListener<any>[]>();
和任何其他引用GameEvent
或GameEventListener
的地方需要相应地修改:
public fireEvent<K extends GameEventType>(event: GameEvent<K>) { /*impl */ }
这是“最小”重构,它只更改类型而不更改任何运行时代码,并在某些内容触及 listenerMap
时放弃一些类型安全。例如,在 on()
中,您可以将 get(eventType)
替换为某些特定类型,例如 get(GameEventType.EVENT_THREE)
并且编译器不知道其中存在问题。
const existing = this.listenerMap.get(GameEventType.EVENT_THREE) ??
[]; // no error, oops
如果你真的关心类型安全,你可以看看 , but it's much easier to just use a plain old object type like {[K in GameEventType]?: GameEventListener<K>[]
, and refactor the other types to be distributive object types as coined in microsoft/TypeScript#47109 来有效地处理相关泛型:
interface GameEventMap {
[GameEventType.EVENT_ONE]: { parameter: string };
[GameEventType.EVENT_TWO]: { anotherParam: SomeType };
[GameEventType.EVENT_THREE]: {}
}
type GameEvent<K extends GameEventType> = { [P in K]: GameEventMap[P] & { type: P } }[K]
type GameEventListener<K extends GameEventType> = (event: GameEvent<K>) => void;
type ListenerMap<K extends GameEventType> = { [P in K]?: GameEventListener<K>[] };
然后 listenerMap
将是 ListenerMap<K>
类型,对于您关心的任何 K
:
export class GameObserver {
private listenerMap: ListenerMap<GameEventType> = {}
public on<K extends GameEventType>(eventType: K, listener: GameEventListener<K>) {
const listenerMap: ListenerMap<K> = this.listenerMap;
let listeners = listenerMap[eventType];
if (!listeners) {
listeners = [];
listenerMap[eventType] = listeners;
}
listeners.push(listener);
}
public off<K extends GameEventType>(eventType: K, listener: GameEventListener<K>) {
const listenerMap: ListenerMap<K> = this.listenerMap;
let listeners = listenerMap[eventType];
if (!listeners) {
listeners = [];
}
listeners = listeners.filter(l => l !== listener);
listenerMap[eventType] = listeners;
}
public fireEvent<K extends GameEventType>(event: GameEvent<K>) {
const listenerMap: ListenerMap<K> = this.listenerMap;
let listeners = listenerMap[event.type];
if (!listeners) {
listeners = [];
}
listeners.forEach((l) => l(event));
}
}
这是一种“最大”重构,其中编译器将更能够捕获您的 class 方法中的类型错误,例如类似于之前的 listenerMap.get(GameEventType.EVENT_THREE)
的以下行:
let listeners = listenerMap[GameEventType.EVENT_THREE]; // error!
export enum GameEventType {
EVENT_ONE = 'event-one',
EVENT_TWO = 'event-two',
EVENT_THREE = 'event-three'
}
export type GameEvent =
| { type: GameEventType.EVENT_ONE; parameter: string }
| { type: GameEventType.EVENT_TWO; anotherParam: SomeType }
| { type: GameEventType.EVENT_THREE; };
type GameEventListener = (event: GameEvent) => void;
export class GameObserver {
private listenerMap = new Map<GameEventType, GameEventListener[]>();
public on(eventType: GameEventType, listener: GameEventListener) {
const existing = this.listenerMap.get(eventType) ?? [];
existing.push(listener);
this.listenerMap.set(eventType, existing);
}
public off(eventType: GameEventType, listener: GameEventListener) {
let existing = this.listenerMap.get(eventType) ?? [];
if (existing.length) {
existing = existing.filter((l) => l !== listener);
this.listenerMap.set(eventType, existing);
}
}
public fireEvent(event: GameEvent) {
const listeners = this.listenerMap.get(event.type) ?? [];
listeners.forEach((l) => l(event));
}
}
考虑上面的事件侦听器 class。当你注册一个事件时,你提供你关心的事件类型和一个回调来接收事件对象。触发事件时,通过给 type
属性 正确的 GameEventType
枚举值,它将建议该事件对象的其他道具。不错
不太好的是,消费者仍然必须确保事件类型符合预期,才能访问对象中的其他道具。即使消费者已经注册接收 GameEventType.EVENT_ONE
的事件,并且即使它只会在触发该事件类型时接收事件,消费者仍然必须说 if (event.type === GameEventType.EVENT_ONE)
才能到达整个事件对象中的道具。
gameObserver.on(GameEventType.EVENT_ONE, (event: GameEvent) => {
// This won't work - cannot access other props of event object
console.log(event.parameter)
// Must do this
if (event.type === GameEventType.EVENT_ONE) {
// In order to see the other props linked to that enum value under type
console.log(event.parameter)
}
});
我真正喜欢的是:
- 当通过
on
方法注册一个事件监听器时,它会警告不要分配一个参数与事件属性不匹配的回调函数 - 触发事件时,它会根据事件类型知道事件对象道具,并在您输入时自动 completes/suggests 这些道具名称
- 消费时,回调只有事件的属性而不是类型(我们知道类型,我们注册了这个回调!)因此不必检查事件类型以获得道具
我以不同的方式处理了后一点,使用 any
,它在前两点上失败了(并且希望避免任何)。
如有任何关于此问题的建议,我们将不胜感激!感谢阅读。
至少,您需要在事件的类型 K
中创建 on()
和 off()
方法 generic,以便编译器可以表示第一个和第二个参数之间的联系。让我们把 GameEvent
变成 GameEvent<K>
:
export type GameEvent<K extends GameEventType> = Extract<
| { type: GameEventType.EVENT_ONE; parameter: string }
| { type: GameEventType.EVENT_TWO; anotherParam: SomeType }
| { type: GameEventType.EVENT_THREE; }, { type: K }>
这仅对 type
属性 为 K
的成员使用 the Extract<T, U>
utility type to filter the original union。因此 GameEvent<GameEventType.EVENT_TWO>
将具体为 {type: GameEvenetType.EVENT_TWO; anotherParam: SomeType}
。而GameEventListener
对应的变化是:
type GameEventListener<K extends GameEventType> =
(event: GameEvent<K>) => void;
现在我们可以在on()
和off()
中使用K
:
public on<K extends GameEventType>(eventType: K, listener: GameEventListener<K>) {
// same implementation
}
public off<K extends GameEventType>(eventType: K, listener: GameEventListener<K>) {
// same implementation
}
您可以在调用以下方法之一时验证其行为是否符合要求:
const gameObserver = new GameObserver()
gameObserver.on(GameEventType.EVENT_ONE, (event) => {
console.log(event.parameter.toUpperCase()) // okay
});
您确实需要稍微调整您的其他代码来处理这些泛型。最棘手的地方是你的 listenerMap
属性,它使用 a Map
object。不幸的是,TypeScript 的 Map<K, V>
类型给每个 属性 相同的类型 V
,无论您使用的是 K
中的哪个键。最少的工作量但最不安全的是给它一个像 Map<GameEventType, GameEventListener<any>[]>
这样的类型,而不用担心潜在的不匹配:
private listenerMap = new Map<GameEventType, GameEventListener<any>[]>();
和任何其他引用GameEvent
或GameEventListener
的地方需要相应地修改:
public fireEvent<K extends GameEventType>(event: GameEvent<K>) { /*impl */ }
这是“最小”重构,它只更改类型而不更改任何运行时代码,并在某些内容触及 listenerMap
时放弃一些类型安全。例如,在 on()
中,您可以将 get(eventType)
替换为某些特定类型,例如 get(GameEventType.EVENT_THREE)
并且编译器不知道其中存在问题。
const existing = this.listenerMap.get(GameEventType.EVENT_THREE) ??
[]; // no error, oops
如果你真的关心类型安全,你可以看看 {[K in GameEventType]?: GameEventListener<K>[]
, and refactor the other types to be distributive object types as coined in microsoft/TypeScript#47109 来有效地处理相关泛型:
interface GameEventMap {
[GameEventType.EVENT_ONE]: { parameter: string };
[GameEventType.EVENT_TWO]: { anotherParam: SomeType };
[GameEventType.EVENT_THREE]: {}
}
type GameEvent<K extends GameEventType> = { [P in K]: GameEventMap[P] & { type: P } }[K]
type GameEventListener<K extends GameEventType> = (event: GameEvent<K>) => void;
type ListenerMap<K extends GameEventType> = { [P in K]?: GameEventListener<K>[] };
然后 listenerMap
将是 ListenerMap<K>
类型,对于您关心的任何 K
:
export class GameObserver {
private listenerMap: ListenerMap<GameEventType> = {}
public on<K extends GameEventType>(eventType: K, listener: GameEventListener<K>) {
const listenerMap: ListenerMap<K> = this.listenerMap;
let listeners = listenerMap[eventType];
if (!listeners) {
listeners = [];
listenerMap[eventType] = listeners;
}
listeners.push(listener);
}
public off<K extends GameEventType>(eventType: K, listener: GameEventListener<K>) {
const listenerMap: ListenerMap<K> = this.listenerMap;
let listeners = listenerMap[eventType];
if (!listeners) {
listeners = [];
}
listeners = listeners.filter(l => l !== listener);
listenerMap[eventType] = listeners;
}
public fireEvent<K extends GameEventType>(event: GameEvent<K>) {
const listenerMap: ListenerMap<K> = this.listenerMap;
let listeners = listenerMap[event.type];
if (!listeners) {
listeners = [];
}
listeners.forEach((l) => l(event));
}
}
这是一种“最大”重构,其中编译器将更能够捕获您的 class 方法中的类型错误,例如类似于之前的 listenerMap.get(GameEventType.EVENT_THREE)
的以下行:
let listeners = listenerMap[GameEventType.EVENT_THREE]; // error!