Skip to content

TypeScript

PubSubJS is built with TypeScript and provides comprehensive type inference.

Event definitions automatically infer types:

import { defineEvent, type EventNames, type EventPayload } from "@pubsubjs/core";
import { z } from "zod";
const events = defineEvent([
{
name: "user.created",
schema: z.object({
userId: z.string(),
email: z.string().email(),
}),
},
{
name: "order.placed",
schema: z.object({
orderId: z.string(),
total: z.number(),
}),
},
]);
// Extract event types
type Events = typeof events;
// Get all event names as union
type AllEventNames = EventNames<Events>;
// => "user.created" | "order.placed"
// Get payload type for specific event
type UserCreatedPayload = EventPayload<Events, "user.created">;
// => { userId: string; email: string }
type OrderPlacedPayload = EventPayload<Events, "order.placed">;
// => { orderId: string; total: number }
const publisher = new Publisher({ events, transport });
// Type-checked event names
await publisher.publish("user.created", payload); // OK
await publisher.publish("typo", payload); // Error!
// Type-checked payloads
await publisher.publish("user.created", {
userId: "123",
email: "test@example.com",
}); // OK
await publisher.publish("user.created", {
userId: 123, // Error: number not string
email: "test@example.com",
});
await publisher.publish("user.created", {
userId: "123",
// Error: missing email
});
const subscriber = new Subscriber({ events, transport });
// Handler payload is typed
subscriber.on("user.created", (payload) => {
payload.userId; // string
payload.email; // string
payload.invalid; // Error: Property does not exist
});
// Context is typed
subscriber.on("user.created", (payload, { ctx }) => {
ctx.messageId; // string
ctx.timestamp; // Date
});
interface MyContext {
messageId: string;
timestamp: Date;
userId: string;
roles: string[];
}
const subscriber = new Subscriber<typeof events, MyContext>({
events,
transport,
contextFactory: (metadata) => ({
messageId: metadata.messageId,
timestamp: new Date(),
userId: metadata.userId as string,
roles: (metadata.roles as string[]) || [],
}),
});
subscriber.on("user.created", (payload, { ctx }) => {
ctx.userId; // string
ctx.roles; // string[]
});
import type { SubscribeMiddleware } from "@pubsubjs/core";
// Middleware with typed events and context
const myMiddleware: SubscribeMiddleware<typeof events, MyContext> = async (
eventName, // "user.created" | "order.placed"
payload, // unknown (validated by the time it reaches handler)
context, // MyContext
next
) => {
console.log(`User ${context.userId} processing ${eventName}`);
await next();
};
import type { HandlerMap } from "@pubsubjs/core";
// Type-safe handler map
const handlers: HandlerMap<typeof events> = {
"user.created": (payload) => {
// payload is typed as { userId: string; email: string }
},
"order.placed": (payload) => {
// payload is typed as { orderId: string; total: number }
},
};
subscriber.onMany(handlers);
import type { PublisherInterface, EventRegistry } from "@pubsubjs/core";
// Use in dependency injection
class NotificationService {
constructor(private publisher: PublisherInterface<typeof events>) {}
async notifyUserCreated(userId: string, email: string) {
await this.publisher.publish("user.created", { userId, email });
}
}
import { z } from "zod";
import type { InferOutput } from "@pubsubjs/core";
const userSchema = z.object({
userId: z.string(),
email: z.string().email(),
profile: z.object({
name: z.string(),
age: z.number().optional(),
}),
});
// Extract type from schema
type User = InferOutput<typeof userSchema>;
// => { userId: string; email: string; profile: { name: string; age?: number } }

PubSubJS works with any Standard Schema compatible library:

// Zod
import { z } from "zod";
const zodSchema = z.object({ name: z.string() });
// Valibot
import * as v from "valibot";
const valibotSchema = v.object({ name: v.string() });
// Both work with defineEvent
const events = defineEvent([
{ name: "event1", schema: zodSchema },
{ name: "event2", schema: valibotSchema },
]);
const events = defineEvent([
{
name: "notification",
schema: z.discriminatedUnion("type", [
z.object({
type: z.literal("email"),
to: z.string().email(),
subject: z.string(),
}),
z.object({
type: z.literal("sms"),
phone: z.string(),
message: z.string(),
}),
z.object({
type: z.literal("push"),
deviceId: z.string(),
title: z.string(),
}),
]),
},
]);
subscriber.on("notification", (payload) => {
// TypeScript narrows the type based on discriminator
if (payload.type === "email") {
payload.to; // string (email)
payload.subject; // string
} else if (payload.type === "sms") {
payload.phone; // string
payload.message; // string
} else {
payload.deviceId; // string
payload.title; // string
}
});
import { z } from "zod";
// Create branded types for type safety
const UserId = z.string().brand("UserId");
const OrderId = z.string().brand("OrderId");
type UserId = z.infer<typeof UserId>;
type OrderId = z.infer<typeof OrderId>;
const events = defineEvent([
{
name: "order.placed",
schema: z.object({
orderId: OrderId,
userId: UserId,
total: z.number(),
}),
},
]);
// Type-safe IDs
const userId: UserId = "user-123" as UserId;
const orderId: OrderId = "order-456" as OrderId;
// Can't mix up IDs
await publisher.publish("order.placed", {
orderId: userId, // Error! UserId is not OrderId
userId: orderId, // Error! OrderId is not UserId
total: 99.99,
});

Extend PubSubJS types:

types.d.ts
declare module "@pubsubjs/core" {
interface TransportMetadata {
userId?: string;
traceId?: string;
source?: string;
}
}