Skip to main content

Getting started

A type-safe communication channel for sending and receiving messages between different contexts in a TypeScript environment.

Features

  • Full TypeScript support with type checking for messages
  • Multiple transport implementations (EventTarget, PostMessage, your own custom implementation)
  • Request/response with abort and timeout
  • Simple, lightweight API
  • Zero dependencies

Video presentation

Installation

pnpm add typed-channel

Core Concepts

typed-channel provides a simple API for creating type-safe communication channels between different contexts in your application. This is particularly useful for:

  • Communication between main thread and Web Workers
  • Cross-frame communication using postMessage
  • Communication between different parts of your application via events
  • Any other event-based communication

Messages and requests

The library has two factories. Both sit on the same transports, so you pick one by the kind of traffic you need.

createTypedChannel carries messages. A message is one-way: you send it with emit and receive it with on, and nobody answers.

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

type Messages = { notify: { message: string } };

const channel = createTypedChannel(createEventTargetTransport<Messages>());

channel.on("notify", ({ message }) => console.log(message));
channel.emit("notify", { message: "Application is ready" });

createTypedRpcChannel adds requests on top. A request is a message the sender waits on: exactly one response, or a failure, comes back for it. It gives you request and handle next to emit and on.

import { createTypedRpcChannel, requests } from "typed-channel";
import { createPostMessageTransport } from "typed-channel/transports/postMessage";

type WorkerRequests = { compute: (params: { steps: number }) => number };
type NoRequests = Record<never, never>;

// First map: what this side handles. Second map: what it calls on the peer.
const channel = createTypedRpcChannel(
  createPostMessageTransport<Messages>(worker),
  requests<NoRequests, WorkerRequests>(),
);

// Wait for the response, and stop waiting after 5 seconds
const total = await channel.request("compute", { steps: 6 }, { signal: AbortSignal.timeout(5000) });

Why two factories

Requests cost code: ids, a pending map, abort handling, error rebuilding. Many projects only send messages and never need any of it. Two factories keep that code out of their bundle.

Minified and compressed with brotli:

Import Size
createTypedChannel 271 B
createTypedRpcChannel + requests 994 B
both together 997 B
createTypedChannel + createPostMessageTransport 341 B

A project that only sends messages pays 271 B instead of 994 B.

The last two rows matter once you already use requests somewhere. Both factories share the same core, so adding createTypedChannel next to createTypedRpcChannel costs 3 B. Pick the factory per channel, by what that channel needs. A channel that only sends messages should use createTypedChannel even when the rest of the app uses requests: it is free, and its API has no request and handle to ignore.

Examples

You can find working examples in the examples directory on GitHub:

License

MIT