Skip to main content

Messages

A message is one-way: you send it and nobody answers. createTypedChannel is the factory for messages. It gives you emit to send one and on to receive one, both checked against the message map you declare.

Step 1: Define your message types

// Define the messages that can be sent through the channel
type Messages = {
  // Message name : payload type
  notify: { message: string };
  clear: never; // Use 'never' for messages with no payload
};

Step 2: Choose a transport

import { createTypedChannel } from "typed-channel";
import { createEventTargetTransport } from "typed-channel/transports/eventTarget";

// Create a transport appropriate for your use case
const transport = createEventTargetTransport<Messages>();
// Create the channel with your type definitions
const channel = createTypedChannel(transport);

Transports lists the ones that ship with the library, and shows how to write your own.

Step 3: Send and receive messages

// Type-safe event handling
const unsubscribeNotify = channel.on("notify", ({ message }) => {
  console.log(`Received notification with message: ${message}`);
});
const unsubscribeClear = channel.on("clear", () => {
  console.clear();
});

// Type-safe event emission
channel.emit("notify", { message: "Application is ready" });
setTimeout(() => {
  channel.emit("clear");
  // Remove all listeners
  unsubscribeNotify();
  unsubscribeClear();
}, 1000);

Unidirectional vs Bidirectional Transports

Transports can be configured in two ways:

  • Unidirectional: Same message types in both directions
  • Bidirectional: Different message types for incoming and outgoing communications

Unidirectional transports are ideal for event buses within the same context, while bidirectional transports excel when communicating between different contexts (like main thread and worker).

Here’s how to use both approaches:

// Unidirectional: Same message types for both directions (default)
const uniTransport = createEventTargetTransport<{ start: never; stop: never }>();

// Can receive and emit the same set of messages
uniTransport.on("start", () => {});
uniTransport.on("stop", () => {});
uniTransport.emit("start");
uniTransport.emit("stop");

// Bidirectional: Different message types for inbound and outbound
type InboundMessages = {
  status: { code: number; message: string };
  data: { items: unknown[] };
};

type OutboundMessages = {
  fetch: { id: string };
  cancel: never;
};

// Explicitly defining different types for incoming and outgoing messages
const biTransport = createPostMessageTransport<InboundMessages, OutboundMessages>(worker);

// Can receive only inbound messages
biTransport.on("status", ({ code, message }) => {});
biTransport.on("data", ({ items }) => {});

// Can emit only outbound messages
biTransport.emit("fetch", { id: "123" });
biTransport.emit("cancel");

Multiple Transports

You can combine multiple transports to send messages to different targets:

import { createTypedChannel } from "typed-channel";
import { createEventTargetTransport } from "typed-channel/transports/eventTarget";
import { createPostMessageTransport } from "typed-channel/transports/postMessage";

const broadcastChannel = new BroadcastChannel("example-channel");
const broadcastTransport = createPostMessageTransport<Messages>(broadcastChannel);
const localTransport = createEventTargetTransport<Messages>();

// Messages will be sent to all tabs, including current one
const channel = createTypedChannel([localTransport, broadcastTransport]);

createTypedRpcChannel takes a single transport instead of a list, because a request goes to one place and its answer comes back from there. See Requests.