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: ProcessorCheckpoint; // 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 = string;

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 ​