Last active
January 24, 2023 05:13
-
-
Save sidola/3b267f21c872e449ef4bbdae9e2baeab to your computer and use it in GitHub Desktop.
Basic typesafe pub-sub implementation in Typescript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* ------------------------------------------ | |
Alternative impl. as a class can be found here: https://gist.github.com/sidola/eaf987d8c4c7e8445b61dc07c33a842f | |
Has a way smaller footprint and less typescript magic to grasp. | |
------------------------------------------ */ | |
/** | |
* Defines the function type of the publish function. | |
* | |
* Extracts the keys from `E` as valid event types, and the matching | |
* property as the payload. | |
*/ | |
type PubTypeFn<E> = <Key extends string & keyof E>( | |
event: Key, | |
message: E[Key] | |
) => void | |
/** | |
* Defines the function type for the subscribe function. | |
* | |
* Extracts the keys from `E` as valid event types, and the matching | |
* property as the payload to the callback function. | |
* | |
* Returns the given callback. | |
*/ | |
type SubTypeFn<E> = <Key extends string & keyof E>( | |
event: Key, | |
fn: (message: E[Key]) => void | |
) => (message: E[Key]) => void | |
/** | |
* Defines the function type for the unsubscribe function. | |
* | |
* Extracts the keys from `E` as valid event types, and the matching | |
* property as the payload to the callback function. | |
*/ | |
type UnsubTypeFn<E> = <Key extends string & keyof E>( | |
event: Key, | |
fn: (message: E[Key]) => void | |
) => void | |
/** | |
* Tie everything together. | |
*/ | |
type PubSubType<E> = { | |
publish: PubTypeFn<E> | |
subscribe: SubTypeFn<E> | |
unsubscribe: UnsubTypeFn<E> | |
} | |
/** | |
* Creates a new PubSub instance, the `E` type parameter should be a | |
* type enumerating all the available events and their payloads. | |
* | |
* @example | |
* type Events = { | |
* warn: { message: string }, | |
* error: { message: string } | |
* } | |
* | |
* const pubSub = PubSub<Events>() | |
* const subHandle = pubSub.subscribe('warn', (message) => { | |
* console.warn(message) | |
* }) | |
* | |
* pubSub.publish('warn', { message: "Something bad happened!" }) | |
* pubSub.unsubscribe('warn', subHandle) | |
*/ | |
export function PubSub<E>(): PubSubType<E> { | |
// This any[] is our list of handlers functions. We don't have the | |
// necessary type information in here which is why it's any-typed. | |
const handlers: { [key: string]: any[] } = {} | |
return { | |
publish: (event, msg) => { | |
handlers[event].forEach(h => h(msg)) | |
}, | |
subscribe: (event, callback) => { | |
const list = handlers[event] ?? [] | |
list.push(callback) | |
handlers[event] = list | |
return callback | |
}, | |
unsubscribe: (event, callback) => { | |
let list = handlers[event] ?? [] | |
list = list.filter(h => h !== callback) | |
handlers[event] = list | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@crobinson42 Hey, thanks for the heads up, it seems I broke it in revision 3. I've fixed it again now.
The problem was extracting a fresh key inside
MessageFn
, like so:This meant we lost the key information when using this type in
SubTypeFn
and instead got a funky union-type param. The fix seems indeed to be to drop theMessageFn
type and just derive everything where we need it.This means we need to make our
handlers
arrayany[]
typed however, since we obviously won't have the key info there, but that's fine, we don't need the type of our handlers.To solve the
unsubscribe
problem you identified, I opted to havesubscribe
return the handler given to it. This gives us a reference to use when callingunsubscribe
, and as an added bonus we get some type safety in the form of this: