Add draft of TypeScript typesafe event emitter post
This commit is contained in:
parent
d5f478b3c6
commit
3c905aa1d7
23
code/typescript-emitter/js1.js
Normal file
23
code/typescript-emitter/js1.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
class EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
this.handlers = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event) {
|
||||||
|
this.handlers[event]?.forEach(h => h());
|
||||||
|
}
|
||||||
|
|
||||||
|
addHandler(event, handler) {
|
||||||
|
if(!this.handlers[event]) {
|
||||||
|
this.handlers[event] = [handler];
|
||||||
|
} else {
|
||||||
|
this.handlers[event].push(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
emitter.addHandler("start", () => console.log("Started!"));
|
||||||
|
emitter.addHandler("end", () => console.log("Ended!"));
|
||||||
|
emitter.emit("end");
|
||||||
|
emitter.emit("start");
|
23
code/typescript-emitter/js2.js
Normal file
23
code/typescript-emitter/js2.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
class EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
this.handlers = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event, value) {
|
||||||
|
this.handlers[event]?.forEach(h => h(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
addHandler(event, handler) {
|
||||||
|
if(!this.handlers[event]) {
|
||||||
|
this.handlers[event] = [handler];
|
||||||
|
} else {
|
||||||
|
this.handlers[event].push(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
emitter.addHandler("numberChange", n => console.log("New number value is: ", n));
|
||||||
|
emitter.addHandler("stringChange", s => console.log("New string value is: ", s));
|
||||||
|
emitter.emit("numberChange", 1);
|
||||||
|
emitter.emit("stringChange", "3");
|
27
code/typescript-emitter/ts.ts
Normal file
27
code/typescript-emitter/ts.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
class EventEmitter<T> {
|
||||||
|
private handlers: { [eventName in keyof T]?: ((value: T[eventName]) => void)[] }
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.handlers = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<K extends keyof T>(event: K, value: T[K]): void {
|
||||||
|
this.handlers[event]?.forEach(h => h(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
addHandler<K extends keyof T>(event: K, handler: (value: T[K]) => void): void {
|
||||||
|
if(!this.handlers[event]) {
|
||||||
|
this.handlers[event] = [handler];
|
||||||
|
} else {
|
||||||
|
this.handlers[event].push(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitter = new EventEmitter<{ numberChange: number, stringChange: string }>();
|
||||||
|
emitter.addHandler("numberChange", n => console.log("New number value is: ", n));
|
||||||
|
emitter.addHandler("stringChange", s => console.log("New string value is: ", s));
|
||||||
|
emitter.emit("numberChange", 1);
|
||||||
|
emitter.emit("stringChange", "3");
|
||||||
|
emitter.emit("numberChange", "1");
|
||||||
|
emitter.emit("stringChange", 3);
|
120
content/blog/typescript_typesafe_events.md
Normal file
120
content/blog/typescript_typesafe_events.md
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
---
|
||||||
|
title: "Type-Safe Event Emitter in TypeScript"
|
||||||
|
date: 2021-09-04T17:18:49-07:00
|
||||||
|
draft: true
|
||||||
|
tags: ["TypeScript"]
|
||||||
|
---
|
||||||
|
|
||||||
|
I've been playing around with TypeScript recently, and enjoying it too.
|
||||||
|
Nearly all of my compile-time type safety desires have been accomodated
|
||||||
|
by the language, and in a rather intuitive and clean way. Today, I'm going
|
||||||
|
to share a little trick I've discovered which allows me to do something that
|
||||||
|
I suspect would normally require [dependent types](https://en.wikipedia.org/wiki/Dependent_type).
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
Suppose you want to write a class that emits events. Clients can then install handlers,
|
||||||
|
functions that are notified whenever an event is emitted. Easy enough; in JavaScript,
|
||||||
|
this would look something like the following:
|
||||||
|
|
||||||
|
{{< codelines "JavaScript" "typescript-emitter/js1.js" 1 17 >}}
|
||||||
|
|
||||||
|
We can even write some code to test that this works (just to ease my nerves):
|
||||||
|
|
||||||
|
{{< codelines "JavaScript" "typescript-emitter/js1.js" 19 23 >}}
|
||||||
|
|
||||||
|
As expected, we get:
|
||||||
|
|
||||||
|
```
|
||||||
|
Started!
|
||||||
|
Ended!
|
||||||
|
```
|
||||||
|
|
||||||
|
As you probably guessed, we're going to build on this problem a little bit.
|
||||||
|
In certain situations, you don't just care that an event occured; you also
|
||||||
|
care about additional event data. For instance, when a number changes, you
|
||||||
|
may want to know the number's new value. In JavaScript, this is a trivial change:
|
||||||
|
|
||||||
|
{{< codelines "JavaScript" "typescript-emitter/js2.js" 1 17 "hl_lines = 6-8" >}}
|
||||||
|
|
||||||
|
That's literally it. Once again, let's ensure that this works by sending two new events:
|
||||||
|
`stringChanged` and `numberChanged`.
|
||||||
|
|
||||||
|
{{< codelines "JavaScript" "typescript-emitter/js2.js" 19 23 >}}
|
||||||
|
|
||||||
|
The result of this code is once again unsurprising:
|
||||||
|
|
||||||
|
```
|
||||||
|
New number value is: 1
|
||||||
|
New string value is: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
But now, how would one go about encoding this in TypeScript? In particular, what is the
|
||||||
|
type of a handler? We could, of course, give each handler the type `(value: any) => void`.
|
||||||
|
This, however, makes handlers unsafe. We could very easily write:
|
||||||
|
|
||||||
|
```TypeScript
|
||||||
|
emitter.addHandler("numberChanged", (value: string) => {
|
||||||
|
console.log("String length is", value.length);
|
||||||
|
});
|
||||||
|
emitted.emit("numberChanged", 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
Which would print out:
|
||||||
|
|
||||||
|
```
|
||||||
|
String length is undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
No, I don't like this. TypeScript is supposed to be all about adding type safety to our code,
|
||||||
|
and this is not at all type safe. We could do all sorts of weird things! There is a way,
|
||||||
|
however, to make this use case work.
|
||||||
|
|
||||||
|
### The Solution
|
||||||
|
|
||||||
|
Let me show you what I came up with:
|
||||||
|
|
||||||
|
{{< codelines "TypeScript" "typescript-emitter/ts.ts" 1 19 "hl_lines=1 2 8 12">}}
|
||||||
|
|
||||||
|
The important changes are on lines 1, 2, 8, and 12 (highlighted in the above code block).
|
||||||
|
Let's go through each one of them step-by-step.
|
||||||
|
|
||||||
|
* __Line 1__: Parameterize the `EventEmitter` by some type `T`. We will use this type `T`
|
||||||
|
to specify the exact kind of events that our `EventEmitter` will be able to emit and handle.
|
||||||
|
Specifically, this type will be in the form `{ event: EventValueType }`. For example,
|
||||||
|
for a `mouseClick` event, we may write `{ mouseClick: { x: number, y: number }}`.
|
||||||
|
* __Line 2__: Add a proper type to `handlers`. This requires several ingredients of its own:
|
||||||
|
* We use [index signatures](https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures)
|
||||||
|
to limit the possible names to which handlers can be assigned. We limit these names
|
||||||
|
to the keys of our type `T`; in the preceding example, `keyof T` would be `"mouseClick"`.
|
||||||
|
* We also limit the values: `T[eventName]` retrieves the type of the value associated with
|
||||||
|
key `eventName`. In mouse example, this type would be `{ x: number, y: number }`. We require
|
||||||
|
that a key can only be associated with an array of functions to void, each of which accepts
|
||||||
|
`T[K]` as first argument. This is precisely what we want; for example, `mouseClick` would map to
|
||||||
|
an array of functions that accept the mouse click location.
|
||||||
|
* __Line 8__: We restrict the type of `emit` to only accept values that correspond to the keys
|
||||||
|
of the type `T`. We can't simply write `event: keyof T`, because this would not give us enough
|
||||||
|
information to retrieve the type of `value`: if `event` is just `keyof T`,
|
||||||
|
then `value` can be any of the values of `T`. Instead, we use generics; this way, when the
|
||||||
|
function is called with `"mouseClick"`, the type of `K` is inferred to also be `"mouseClick"`, which
|
||||||
|
gives TypeScript enough information to narrow the type of `value`.
|
||||||
|
* __Line 12__: We use the exact same trick here as we did on line 8.
|
||||||
|
|
||||||
|
Let's give this a spin with our `numberChanged`/`stringChanged` example from earlier:
|
||||||
|
|
||||||
|
{{< codelines "TypeScript" "typescript-emitter/ts.ts" 21 27 >}}
|
||||||
|
|
||||||
|
The function calls on lines 24 and 25 are correct, but the subsequent two (on lines 26 and 27)
|
||||||
|
are not, as they attempt to emit the _opposite_ type of the one they're supposed to. And indeed,
|
||||||
|
TypeScript complains about only these two lines:
|
||||||
|
|
||||||
|
```
|
||||||
|
code/typescript-emitter/ts.ts:26:30 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
|
||||||
|
26 emitter.emit("numberChange", "1");
|
||||||
|
~~~
|
||||||
|
code/typescript-emitter/ts.ts:27:30 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
|
||||||
|
27 emitter.emit("stringChange", 3);
|
||||||
|
~
|
||||||
|
Found 2 errors.
|
||||||
|
```
|
||||||
|
|
||||||
|
And there you have it!
|
Loading…
Reference in New Issue
Block a user