Type Safety
cmd-ipc provides full TypeScript type safety through Valibot schemas. This gives you compile-time type checking and runtime validation.
Loose vs Strict Mode
Section titled “Loose vs Strict Mode”cmd-ipc operates in two modes depending on whether you provide schemas:
Loose Mode
Section titled “Loose Mode”Without schemas, any command or event ID is accepted:
const registry = new CommandRegistry({ id: 'main' })
// No type checking - any command ID and payload acceptedawait registry.executeCommand('anything', { any: 'payload' })registry.emitEvent('any.event', { data: 123 })Strict Mode
Section titled “Strict Mode”With schemas, you get full type checking—only commands and events defined in the schemas are allowed:
const registry = new CommandRegistry({ id: 'main', schemas: { commands: CommandSchema, events: EventSchema, },})
// TypeScript enforces correct typesawait registry.executeCommand('user.create', { name: 'John', email: 'j@x.com' })await registry.executeCommand('unknown.cmd', {}) // TypeScript error!
registry.emitEvent('user.created', { id: '123', name: 'John' })registry.emitEvent('unknown.event', {}) // TypeScript error!Mixed Mode
Section titled “Mixed Mode”You can use strict mode for one schema type while keeping the other loose. For example, to have strict commands but loose events:
const registry = new CommandRegistry({ id: 'main', schemas: { commands: CommandSchema, // events not provided - loose mode for events },})
// Commands are strictly typedawait registry.executeCommand('user.create', { name: 'John', email: 'j@x.com' })
// Events accept any ID and payloadregistry.emitEvent('any.event', { data: 123 })Schemas
Section titled “Schemas”Command Schemas
Section titled “Command Schemas”Define your commands with Valibot schemas:
import * as v from 'valibot'import type { CommandSchemaMap } from '@coralstack/cmd-ipc'
export const CommandSchema = { 'user.create': { description: 'Creates a new user', request: v.object({ name: v.string(), email: v.pipe(v.string(), v.email()), }), response: v.object({ id: v.string(), name: v.string(), email: v.string(), createdAt: v.date(), }), }, 'user.delete': { description: 'Deletes a user by ID', request: v.object({ id: v.string(), }), response: v.object({ success: v.boolean(), }), },} as const satisfies CommandSchemaMapEvent Schemas
Section titled “Event Schemas”Events can also have schemas:
import type { EventSchemaMap } from '@coralstack/cmd-ipc'
export const EventSchema = { 'user.created': v.object({ id: v.string(), name: v.string(), }), 'user.deleted': v.object({ id: v.string(), }),} as const satisfies EventSchemaMapMerging Schemas
Section titled “Merging Schemas”Combine schemas from multiple sources using the spread operator:
import { LocalCommandSchema } from './local-commands'import { RemoteCommandSchema } from './generated/remote-commands'
// Merge schemasconst MergedSchema = { ...LocalCommandSchema, ...RemoteCommandSchema,} as const
const registry = new CommandRegistry({ id: 'main', schemas: { commands: MergedSchema },})This is particularly useful when working with the CLI to generate schemas from remote services, then merging them with your local schemas.
Type-Safe IDs with defineIds
Section titled “Type-Safe IDs with defineIds”Use the defineIds helper to create type-safe constants for command and event IDs. This ensures consistency throughout your codebase and catches renaming errors at compile time rather than runtime.
The helper converts schema keys to uppercase constants:
- Dots (
.) are converted to underscores (_) - Letters are uppercased
- Private IDs starting with
_retain the leading underscore
import { defineIds } from '@coralstack/cmd-ipc'
// From command schemaexport const CommandIDs = defineIds(CommandSchema)// CommandIDs.USER_CREATE = 'user.create'// CommandIDs.USER_DELETE = 'user.delete'// CommandIDs._INTERNAL_LOG = '_internal.log' (private command)
// From event schemaexport const EventIDs = defineIds(EventSchema)// EventIDs.USER_CREATED = 'user.created'// EventIDs.USER_DELETED = 'user.deleted'Now use these constants instead of string literals:
// Execute commandsawait registry.executeCommand(CommandIDs.USER_CREATE, { name: 'John', email: 'j@x.com' })
// Register handlersregistry.registerCommand(CommandIDs.USER_DELETE, async ({ id }) => { // ...})
// Emit and listen to eventsregistry.emitEvent(EventIDs.USER_CREATED, { id: '123', name: 'John' })registry.onEvent(EventIDs.USER_DELETED, ({ id }) => { // ...})Extracting Types
Section titled “Extracting Types”Create reusable type helpers to extract request and response types from your schemas. This ensures your handlers stay in sync with your schema definitions:
import type { InferCommandSchemaMapType } from '@coralstack/cmd-ipc'
// Infer all command typestype Commands = InferCommandSchemaMapType<typeof CommandSchema>
// Create helper types for handlersexport type CommandRequest<K extends keyof Commands> = Commands[K]['request']export type CommandResponse<K extends keyof Commands> = Promise<Commands[K]['response']>Using with Handlers
Section titled “Using with Handlers”Use the type helpers in your command handlers to ensure they match the schema:
import { Command } from '@coralstack/cmd-ipc'import type { CommandRequest, CommandResponse } from './schemas'import { CommandIDs } from './schemas'
class UserService { @Command(CommandIDs.USER_CREATE) async create( request: CommandRequest<typeof CommandIDs.USER_CREATE> ): CommandResponse<typeof CommandIDs.USER_CREATE> { return { id: '123', name: request.name, email: request.email, createdAt: new Date(), } }}Best Practices
Section titled “Best Practices”- Always use schemas - Type safety is the main benefit of cmd-ipc
- Use
defineIds- Prevents typos and makes refactoring safe - Use
satisfies- Ensures schema structure is correct while preserving literal types - Extract types - Create
CommandRequest/CommandResponsehelpers for handlers - Generate from remote - Use the CLI to generate schemas from remote services
- Share schemas - Import from a common package in monorepos