Skip to main content

Transports

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:

  1. The on method wraps Figma’s message handling convention (where messages arrive via pluginMessage property)
  2. The emit method sends messages to the parent frame with the proper Figma message format
  3. 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.