Skip to content

Event

WARNING

We created this page with the help of the GenAI tool.

We're currently double-checking it to ensure the information is 100% correct and free of hallucinations.

Events are immutable records of facts that have happened in your system. They are the foundation of Event Sourcing.

Overview

In Event Sourcing, events serve dual purposes:

  1. Historical record - What happened in your business process
  2. State source - Events are replayed to rebuild current state

Events are immutable facts. Once recorded, they cannot be changed or deleted.

Type Definition

typescript
type Event<
  EventType extends string = string,
  EventData extends DefaultRecord = DefaultRecord,
  EventMetaData extends DefaultRecord | undefined = undefined,
> = Readonly<{
  type: EventType;
  data: EventData;
  metadata?: EventMetaData;
  kind?: 'Event';
}>;
PropertyTypeDescription
typestringUnique event type name (e.g., 'ProductItemAdded')
dataobjectBusiness data payload (must be a record, not primitive)
metadataobject?Optional infrastructure data (user ID, tenant, timestamps)
kind'Event'?Discriminator for union types with Commands

Basic Usage

Defining Event Types

ts
import type { Event } from '@event-driven-io/emmett';

type ProductItemAddedToShoppingCart = Event<
  'ProductItemAddedToShoppingCart',
  {
    shoppingCartId: string;
    productItem: PricedProductItem;
  }
>;

Union Types for Aggregates

Define all events for an aggregate as a discriminated union:

typescript
import type { Event } from '@event-driven-io/emmett';

type ShoppingCartEvent =
  | Event<
      'ShoppingCartOpened',
      {
        cartId: string;
        clientId: string;
        openedAt: Date;
      }
    >
  | Event<
      'ProductItemAdded',
      {
        productId: string;
        quantity: number;
        price: number;
      }
    >
  | Event<
      'ProductItemRemoved',
      {
        productId: string;
        quantity: number;
      }
    >
  | Event<
      'ShoppingCartConfirmed',
      {
        confirmedAt: Date;
      }
    >
  | Event<
      'ShoppingCartCancelled',
      {
        cancelledAt: Date;
      }
    >;

Creating Events with Factory

Use the event factory function for runtime event creation:

typescript
import { event } from '@event-driven-io/emmett';

const added = event<ProductItemAdded>('ProductItemAdded', {
  productId: 'shoes-1',
  quantity: 2,
  price: 99.99,
});
// Result: { type: 'ProductItemAdded', data: {...}, kind: 'Event' }

Events with Metadata

Metadata carries cross-cutting concerns like user identity or tracing:

typescript
type AuditMetadata = {
  userId: string;
  correlationId: string;
  timestamp: Date;
};

type AuditedProductItemAdded = Event<
  'ProductItemAdded',
  { productId: string; quantity: number; price: number },
  AuditMetadata
>;

const auditedEvent = event<AuditedProductItemAdded>(
  'ProductItemAdded',
  { productId: 'shoes-1', quantity: 2, price: 99.99 },
  { userId: 'user-123', correlationId: 'req-456', timestamp: new Date() },
);

Read Events

When events are read from the event store, they include additional metadata:

typescript
type ReadEvent<
  EventType extends Event,
  MetadataType extends AnyRecordedMessageMetadata,
> = {
  type: EventType['type'];
  data: EventType['data'];
  metadata: CombinedMetadata<EventType, MetadataType>;
};

Stream Metadata

Events read from a stream include position information:

typescript
type CommonReadEventMetadata = {
  streamName: string; // Stream the event belongs to
  streamPosition: bigint; // Position within the stream (0-indexed)
  createdAt: Date; // When the event was recorded
};

Global Position

Some event stores provide global ordering:

typescript
type ReadEventMetadataWithGlobalPosition = CommonReadEventMetadata & {
  globalPosition: bigint; // Position across all streams
};

Utility Types

Extracting Event Properties

typescript
import type {
  EventTypeOf,
  EventDataOf,
  EventMetaDataOf,
} from '@event-driven-io/emmett';

type ProductItemAdded = Event<'ProductItemAdded', { productId: string }>;

type EventType = EventTypeOf<ProductItemAdded>; // 'ProductItemAdded'
type EventData = EventDataOf<ProductItemAdded>; // { productId: string }
type EventMeta = EventMetaDataOf<ProductItemAdded>; // undefined

Any Event

For generic handlers that accept any event:

typescript
import type { AnyEvent } from '@event-driven-io/emmett';

function logEvent(event: AnyEvent): void {
  console.log(`Event: ${event.type}`, event.data);
}

Best Practices

1. Use Past Tense Names

Events represent facts that have happened:

typescript
// ✅ Good: Past tense
type ProductItemAdded = Event<'ProductItemAdded', {...}>;
type OrderShipped = Event<'OrderShipped', {...}>;

// ❌ Bad: Present/future tense
type AddProductItem = Event<'AddProductItem', {...}>;  // This is a command

2. Include Sufficient Context

Events should be self-contained:

typescript
// ✅ Good: Contains all necessary data
type ProductItemAdded = Event<
  'ProductItemAdded',
  {
    productId: string;
    productName: string; // Denormalized for projections
    quantity: number;
    unitPrice: number;
    totalPrice: number; // Computed at event time
  }
>;

// ❌ Bad: Missing context
type ProductItemAdded = Event<
  'ProductItemAdded',
  {
    productId: string; // Need to look up product details elsewhere
  }
>;

3. Avoid Optional Fields

Events are facts; they should be complete:

typescript
// ✅ Good: Separate event types
type OrderShippedWithTracking = Event<
  'OrderShipped',
  {
    orderId: string;
    trackingNumber: string;
  }
>;

type OrderShippedNoTracking = Event<
  'OrderShippedNoTracking',
  {
    orderId: string;
  }
>;

// ❌ Bad: Optional fields blur meaning
type OrderShipped = Event<
  'OrderShipped',
  {
    orderId: string;
    trackingNumber?: string; // When is it present?
  }
>;

4. Use Readonly Data

Events are immutable by design:

typescript
// The Event type enforces Readonly automatically
type ProductItemAdded = Event<
  'ProductItemAdded',
  {
    productId: string;
    items: ProductItem[]; // Becomes readonly
  }
>;

Type Source

ts
import type { DefaultRecord } from './';
import type {
  AnyRecordedMessageMetadata,
  CombinedMessageMetadata,
  CommonRecordedMessageMetadata,
  RecordedMessage,
  RecordedMessageMetadata,
  RecordedMessageMetadataWithGlobalPosition,
  RecordedMessageMetadataWithoutGlobalPosition,
} from './message';

export type StreamPosition = bigint;
export type GlobalPosition = bigint;

export type Event<
  EventType extends string = string,
  EventData extends DefaultRecord = DefaultRecord,
  EventMetaData extends DefaultRecord | undefined = undefined,
> = Readonly<
  EventMetaData extends undefined
    ? {
        type: EventType;
        data: EventData;
      }
    : {
        type: EventType;
        data: EventData;
        metadata: EventMetaData;
      }
> & { readonly kind?: 'Event' };

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyEvent = Event<any, any, any>;

export type EventTypeOf<T extends Event> = T['type'];
export type EventDataOf<T extends Event> = T['data'];
export type EventMetaDataOf<T extends Event> = T extends { metadata: infer M }
  ? M
  : undefined;

export type CreateEventType<
  EventType extends string,
  EventData extends DefaultRecord,
  EventMetaData extends DefaultRecord | undefined = undefined,
> = Readonly<
  EventMetaData extends undefined
    ? {
        type: EventType;
        data: EventData;
      }
    : {
        type: EventType;
        data: EventData;
        metadata: EventMetaData;
      }
> & { readonly kind?: 'Event' };

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const event = <EventType extends Event<string, any, any>>(
  ...args: EventMetaDataOf<EventType> extends undefined
    ? [type: EventTypeOf<EventType>, data: EventDataOf<EventType>]
    : [
        type: EventTypeOf<EventType>,
        data: EventDataOf<EventType>,
        metadata: EventMetaDataOf<EventType>,
      ]
): EventType => {
  const [type, data, metadata] = args;

  return metadata !== undefined
    ? ({ type, data, metadata, kind: 'Event' } as EventType)
    : ({ type, data, kind: 'Event' } as EventType);
};

export type CombinedReadEventMetadata<
  EventType extends Event = Event,
  EventMetaDataType extends AnyRecordedMessageMetadata =
    AnyRecordedMessageMetadata,
> = CombinedMessageMetadata<EventType, EventMetaDataType>;

export type ReadEvent<
  EventType extends Event = Event,
  EventMetaDataType extends AnyRecordedMessageMetadata =
    AnyRecordedMessageMetadata,
> = RecordedMessage<EventType, EventMetaDataType>;

export type AnyReadEvent<
  EventMetaDataType extends AnyReadEventMetadata = AnyReadEventMetadata,
> = ReadEvent<AnyEvent, EventMetaDataType>;

export type CommonReadEventMetadata = CommonRecordedMessageMetadata;

export type ReadEventMetadata<HasGlobalPosition = undefined> =
  RecordedMessageMetadata<HasGlobalPosition>;

export type AnyReadEventMetadata = AnyRecordedMessageMetadata;

export type ReadEventMetadataWithGlobalPosition =
  RecordedMessageMetadataWithGlobalPosition;

export type ReadEventMetadataWithoutGlobalPosition =
  RecordedMessageMetadataWithoutGlobalPosition;

See Also