A transport moves messages between two contexts. It does not know what a message means: the channel on top of it does. The library ships two transports and lets you write your own.
Available Transports
EventTarget Transport
Uses the standard DOM EventTarget interface for communication.
import { createEventTargetTransport } from "typed-channel/transports/eventTarget";
// Uses a new EventTarget instance by default
const transport = createEventTargetTransport();
// Or provide your own EventTarget
const customTarget = new EventTarget();
const transport = createEventTargetTransport(customTarget);
PostMessage Transport
For communication with Workers, iframes, or other such contexts.
import { createPostMessageTransport } from "typed-channel/transports/postMessage";
// With a Worker
const worker = new Worker("./worker.js");
const transport = createPostMessageTransport(worker);
// With an iframe
const iframe = document.querySelector("iframe");
const transport = createPostMessageTransport(iframe.contentWindow);
BroadcastChannel Transport
For communication between different tabs/windows of the same origin.
import { createPostMessageTransport } from "typed-channel/transports/postMessage";
const broadcastChannel = new BroadcastChannel("example-channel");
const transport = createPostMessageTransport(broadcastChannel);
Custom Transports
You can create custom transports by implementing the TypedChannelTransport interface. This gives you flexibility to adapt typed-channel to any messaging system.
Here’s a simple example of a custom transport structure:
import { type AnyMessageOf, type AnyMessages, type TypedChannelTransport } from "typed-channel";
function createNewTransport<
InboundMessages extends AnyMessages,
OutboundMessages extends AnyMessages,
>(source: EventTarget): TypedChannelTransport<InboundMessages, OutboundMessages> {
function on(handler: (message: AnyMessageOf<InboundMessages>) => void) {
// call the handler with every message coming from the transport
const listener = (event: Event) => {
handler((event as CustomEvent<AnyMessageOf<InboundMessages>>).detail);
};
source.addEventListener("message", listener);
// return a cleanup function
return () => source.removeEventListener("message", listener);
}
function emit(message: AnyMessageOf<OutboundMessages>) {
// pass the emitted message to the transport
source.dispatchEvent(new CustomEvent("message", { detail: message }));
}
return { on, emit };
}
AnyMessageOf<T> now covers request traffic as well as the typed messages of T, so a
transport that spells the message union by hand no longer fits the contract. Use
AnyMessageOf and the transport keeps working.
A transport that only forwards the object as-is needs no change. One that inspects messages
must narrow with "rpc" in message before it reads type as a message name:
function on(handler: (message: AnyMessageOf<Messages>) => void) {
return subscribe((message) => {
if ("rpc" in message) {
// request traffic: forward it untouched
handler(message);
return;
}
console.log(`message ${message.type}`);
handler(message);
});
}
in throws on a value that is not an object, and so does reading message.rpc. A transport
that can receive those must check typeof message === "object" && message !== null first.
Figma Plugin Communication
Here’s a real-world example of custom transport for Figma plugin UI communication:
import {
type AnyMessageOf,
type AnyMessages,
createTypedChannel,
type TypedChannelTransport,
} from "typed-channel";
export type PluginMessages = {
ready: never;
};
export type UIMessages = {
"window:resize": { width: number; height: number };
};
function createFigmaUiTransport<
InboundMessages extends AnyMessages,
OutboundMessages extends AnyMessages,
>(): TypedChannelTransport<InboundMessages, OutboundMessages> {
function on(handler: (message: AnyMessageOf<InboundMessages>) => void) {
const workerMessageHandler = (
e: MessageEvent<{ pluginMessage: AnyMessageOf<InboundMessages> }>,
) => {
handler(e.data.pluginMessage);
};
globalThis.onmessage = workerMessageHandler;
return () => (globalThis.onmessage = null);
}
function emit(message: AnyMessageOf<OutboundMessages>) {
parent.postMessage({ pluginMessage: message }, "*");
}
return { on, emit };
}
// Create the transport with appropriate type parameters
const transport = createFigmaUiTransport<PluginMessages, UIMessages>();
// Create a typed communication channel using our custom transport
export const pluginChannel = createTypedChannel(transport);
This example shows how to create a custom transport for Figma plugins, where:
- The
onmethod wraps Figma’s message handling convention (where messages arrive viapluginMessageproperty) - The
emitmethod sends messages to the parent frame with the proper Figma message format - The transport specifies different types for inbound vs outbound messages
By following these patterns, you can adapt typed-channel to work with any messaging API.