英文:
How to implement Event bus types suggestion in Typescript
问题
Here's the translated code part:
你好,我在尝试实现[TinyEmitter](https://github.com/scottcorgan/tiny-emitter)的类型时遇到了问题。我需要实现两个方法。
第一个:
```typescript
addEventListener(e: string, (...args: any[]) => void): void;
第二个:
emit(e: string, ...args: any[]): void;
但这个解决方案没有提供事件名称和参数的建议。
我需要定义事件类型和事件参数。
类似这样:
type EventMap = [
(e: 'event1', arg1: number, arg2: string, arg3: string) => void,
(e: 'event2', arg1: string, arg2: string) => void,
...
];
实际上,我可以通过以下方式推断事件名称:
type EventParam<I extends number = number> = Parameters<EventMap[I]>[0];
这将推断事件类型 ('event1' | 'event2')
对于其他参数,我尝试了:
type EventArgs<I extends number = number> = EventMap[I] extends ((e: EventParam<I>, ...args: infer P) => any) ? P : never;
如何为 addEventListener
和 emit
函数实现带有类型建议的解决方案?
提前感谢任何建议。
Is there anything else you would like to know or any specific assistance you need with this code?
<details>
<summary>英文:</summary>
Hello I am facing problem when trying to implement types for [TinyEmitter](https://github.com/scottcorgan/tiny-emitter). I need to implement 2 methods.
First:
addEventListener(e: string, (...args: any[]) => void): void;
Second:
emit(e: string, ...args: any[]): void;
But this solution is not suggesting event names, and parameters.
I need to define event types, and event arguments.
Something like:
type EventMap = [
(e: 'event1', arg1: number, arg2: string, arg3: string) => void,
(e: 'event2', arg1: string, arg2: string) => void,
...
];
Actually i can infer event name by:
type EventParam<I extends number = number> = Parameters<EventMap[I]>[0];
this will infer event type `('event1' | 'event2')`
For other parameters i tried:
type EventArgs<I extends number = number> = EventMap[I] extends ((e: EventParam<I>, ...args: infer P) => any ? P : never;
how to implement this for `addEventListener` and `emit` functions with type suggestions ?
Thank you in advance for any advice.
</details>
# 答案1
**得分**: 0
以下是您要翻译的内容:
建议的方法是创建一个实用程序接口,将事件名称(作为键)映射到相应的参数类型列表(作为值):
```typescript
interface EventArgs {
event1: [number, string, string];
event2: [string, string];
// ...
}
然后,您可以将addEventListener()
和emit()
方法定义为具有泛型K
的方法,将e
参数的类型约束为EventArgs
的键:
interface Foo {
addEventListener<K extends keyof EventArgs>(e: K, cb: (...args: EventArgs[K]) => void): void;
emit<K extends keyof EventArgs>(e: K, ...args: EventArgs[K]): void;
}
我将这个接口称为Foo
,因为问题中没有指定。然后,假设我们有一个生成Foo
的实现:
const foo: Foo = makeFoo();
我们可以看到它的行为与期望一样:
// 正确的调用
foo.addEventListener("event1", (n, s1, s2) => {
console.log(n.toFixed(2), s1.toUpperCase(), s2.toLowerCase()); // okay
});
foo.emit("event2", "Abc", "Def"); // okay
foo.emit("event1", Math.PI, "Abc", "Def"); // okay
// 错误的调用
foo.emit("event1", "Abc", "Def"); // error!
foo.addEventListener("event2", (n, s1, s2) => { // error!
console.log(n.toFixed(2), s1.toUpperCase(), s2.toLowerCase())
});
这基本上回答了主要涉及类型的问题,但仍然可以以相当类型安全的方式实现它:
function makeFoo(): Foo {
const listenerMap: { [K in keyof EventArgs]?: ((...args: EventArgs[K]) => void)[] } = {}
const ret: Foo = {
addEventListener<K extends keyof EventArgs>(e: K, cb: (...args: EventArgs[K]) => void) {
const listeners: ((...args: EventArgs[K]) => void)[] = listenerMap[e] ??= [];
listeners.push(cb);
},
emit<K extends keyof EventArgs>(e: K, ...a: EventArgs[K]) {
const listeners: ((...args: EventArgs[K]) => void)[] = listenerMap[e] ?? [];
listeners.forEach(cb => cb(...a))
}
}
return ret;
}
它基本上只是将事件名称映射到事件监听器数组的对象映射,addEventListener
将监听器推送到正确的数组中(如果需要,首先进行初始化),而emit
从正确的数组中调用监听器(除非它不存在)。
英文:
The approach I'd recommend is to make a utility interface which maps the name of the event (as a key) to the corresponding list of argument types (as a value):
interface EventArgs {
event1: [number, string, string];
event2: [string, string];
// ...
}
Then you can define your addEventListener()
and emit()
methods as being generic in K
, the type of the e
argument constrained to be a key of EventArgs
:
interface Foo {
addEventListener<K extends keyof EventArgs>(e: K, cb: (...args: EventArgs[K]) => void): void;
emit<K extends keyof EventArgs>(e: K, ...args: EventArgs[K]): void;
}
I'm calling the interface Foo
since it wasn't specified in the question. Then assuming we have an implementation that makes a Foo
:
const foo: Foo = makeFoo();
We can see that it behaves as desired:
// Good calls
foo.addEventListener("event1", (n, s1, s2) => {
console.log(n.toFixed(2), s1.toUpperCase(), s2.toLowerCase()); // okay
});
foo.emit("event2", "Abc", "Def"); // okay
foo.emit("event1", Math.PI, "Abc", "Def"); // okay
// Bad calls
foo.emit("event1", "Abc", "Def"); // error!
foo.addEventListener("event2", (n, s1, s2) => { // error!
console.log(n.toFixed(2), s1.toUpperCase(), s2.toLowerCase())
});
That essentially answers the question which is primarily about typings; still, it can be implemented like this in a fairly type-safe way:
function makeFoo(): Foo {
const listenerMap: { [K in keyof EventArgs]?: ((...args: EventArgs[K]) => void)[] } = {}
const ret: Foo = {
addEventListener<K extends keyof EventArgs>(e: K, cb: (...args: EventArgs[K]) => void) {
const listeners: ((...args: EventArgs[K]) => void)[] = listenerMap[e] ??= [];
listeners.push(cb);
},
emit<K extends keyof EventArgs>(e: K, ...a: EventArgs[K]) {
const listeners: ((...args: EventArgs[K]) => void)[] = listenerMap[e] ?? [];
listeners.forEach(cb => cb(...a))
}
}
return ret;
}
Essentially it just holds onto an object map from event names to arrays of event listeners, and addEventListener
pushes the listener onto the right array (initializing it first if need be), while emit
calls the listeners from the right array (unless it doesn't exist).
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论