Type-safe Events Map implementation

Strongly-Typed Event Map

Event Map Typing

Event maps are used to accurately predict events and their payloads. They help match an event name with the correct data type. Common uses include analytics (e.g., sending events), event emitters, or logging features.

Event maps are helpful when it's important to maintain data consistency and avoid mistakes by developers.

In this article, I will show you how to create a type-safe Event Map in TypeScript. By using constructs such as mapped types, literal types, and type inference, we gain full control over the structure of the data sent to the event handler function.

Implementing Event Map in TypeScript

// Representing all possible data that can be part of any event
interface EventProperty {
  id: string;
  position: number;
  name: string;
  type: string;
  state: string;
  duration: number;
  quality: 'sd' | 'hd' | 'fhd' | 'uhd';
  volume: number;
  userId: string;
}

// Representing all possible events
enum EventName {
  Info = 'info',
  Play = 'play',
  Resume = 'resume',
  Pause = 'pause',
  Stop = 'stop',
  Buffering = 'buffering',
  Seek = 'seek',
}

// This is a map that assigns an array of keys from EventProperty for each event type, specifying which properties should be used
type EventParamsMap = {
  [P in EventName]: readonly (keyof EventProperty)[];
};

// Static implementation of EventParamsMap. Contains information about what properties are needed for a given event
const eventParamsMap = {
  [EventName.Info]: ['id', 'name', 'state'],
  [EventName.Play]: ['id', 'position', 'state', 'volume', 'quality'],
  [EventName.Resume]: ['id', 'position', 'state', 'volume'],
  [EventName.Pause]: ['id', 'state'],
  [EventName.Stop]: ['id', 'position', 'duration'],
  [EventName.Buffering]: ['id', 'position', 'state'],
  [EventName.Seek]: ['id', 'position', 'userId'],
} satisfies EventParamsMap;

// The EventNameProperties<T> and EventPayloadProperties<T> constructs dynamically build the type of the property object that must be passed to the sendEvent function
type EventNameProperties<T extends EventName> =
  (typeof eventParamsMap)[T][number];

export type EventPayloadProperties<
  T extends EventName
> = { [K in EventNameProperties<T>]: EventProperty[K] };

// Final function
function sendEvent<TEvent extends EventName>(
  event: TEvent,
  properties: EventPayloadProperties<TEvent>
) {
  // Code responsible for data handling...
  // e.g. HttpService.sendEvent(event, properties)
}

// An examples:
sendEvent(EventName.Play, {
  id: 'movie-001',
  position: 0,
  state: 'playing',
  volume: 85,
  quality: 'fhd',
});

sendEvent(EventName.Resume, {
  id: 'movie-001',
  position: 150,
  state: 'playing',
  volume: 80,
});

sendEvent(EventName.Pause, {
  id: 'movie-001',
  state: 'paused',
});

sendEvent(EventName.Buffering, {
  id: 'movie-001',
  position: 200,
  state: 'buffering',
});

sendEvent(EventName.Seek, {
  id: 'movie-001',
  position: 560,
  userId: 'user-789',
});

© 2025 / jakubjereczek.com. All rights reserved.