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-channelnpm install typed-channelyarn add typed-channelbun add typed-channelCore 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: