Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
TypeScriptJavaScriptJune 23, 20264 min read

TypeScript Event Sourcing: Enforcing Immutable Patterns Safely

Master TypeScript Event Sourcing by using mapped types and nominal typing to enforce immutable patterns, keeping your domain logic bug-free and predictable.

TypeScriptEvent SourcingDomain Driven DesignType SafetyImmutable PatternsJavaScriptFrontend

Last month, I was debugging an event-sourcing implementation where a rogue developer accidentally swapped an OrderCreated event with an OrderUpdated command. The runtime error didn't show up until we hit the reporting service, resulting in about three hours of painful data reconciliation. That's when I realized that standard interfaces just aren't enough for the strict requirements of Event Sourcing.

To solve this, I started leveraging TypeScript's advanced type features to enforce structural integrity at compile-time. If you're building a system where state is derived from a stream of events, your types need to be as immutable as your data.

Why Event Sourcing Needs Stronger Types

In a standard CRUD app, your types often just describe the shape of a database row. In Event Sourcing, your types describe a historical record. If you can change an event after it’s been recorded, your entire audit log becomes untrustworthy.

We first tried using simple unions for our events, but that allowed developers to pass commands where events were expected. We needed a way to distinguish between a Command (a request to do something) and an Event (a record of something that happened) even if they shared identical properties.

Using Nominal Typing for Domain Integrity

To differentiate between these concepts, I use nominal typing—often called "branding" or "tagging." As I discussed in my guide on TypeScript Branded Types: Enforcing Domain Integrity at Compile-Time, this prevents accidental assignment of incompatible types.

Here’s how I structure my base types:

TYPESCRIPT
type Brand<K, T> = K & { __brand: T };

type Command<T> = Brand<T, CE9178">'Command'>;
type DomainEvent<T> = Brand<T, CE9178">'Event'>;

interface CreateOrderPayload {
  orderId: string;
  amount: number;
}

type CreateOrderCommand = Command<CreateOrderPayload>;
type OrderCreatedEvent = DomainEvent<CreateOrderPayload>;

By adding that __brand property, TypeScript now sees CreateOrderCommand and OrderCreatedEvent as fundamentally different, even if their inner shapes match. This is a massive win for preventing logic errors during event handlers.

Leveraging Mapped Types for Immutable Patterns

Once you have your branded types, you need to ensure they stay immutable. I use mapped types combined with the readonly modifier to lock down these objects. This is similar to the strategies I use for TypeScript Configuration Patterns: Enforcing Type-Safe Partial Defaults, where strict control over object modification is key.

TYPESCRIPT
type Immutable<T> = {
  readonly [P in keyof T]: T[P] extends object ? Immutable<T[P]> : T[P];
};

// Now every event is deep-frozen by the type system
type ReadonlyEvent<T> = Immutable<DomainEvent<T>>;

Using this Immutable<T> helper, I can guarantee that once an event is created, no part of our application can mutate its properties.

Putting It All Together

In a real-world scenario, you'll want to map your commands to their resulting events. This is where Mapped Types shine. I define a registry that links every command to the specific event it triggers.

TYPESCRIPT
interface EventMap {
  CreateOrder: { payload: CreateOrderPayload; event: OrderCreatedEvent };
  CancelOrder: { payload: { orderId: string }; event: OrderCancelledEvent };
}

type CommandHandler<K extends keyof EventMap> = (
  cmd: Command<EventMap[K][CE9178">'payload']>
) => EventMap[K][CE9178">'event'];

This structure makes the code remarkably self-documenting. If I want to see what happens when I trigger a CreateOrder command, I just look at the EventMap. If I try to return the wrong event type from my handler, the compiler yells at me immediately.

Trade-offs and Lessons Learned

Is this overkill? Sometimes. If you’re working on a tiny prototype, the overhead of defining branded types and mapped registries might feel like friction. I’ve definitely had moments where I wanted to just any my way through a quick fix.

However, when you have a system that has been running for six months with thousands of events, this level of strictness is a lifesaver. It forces you to think about the transition from intent (command) to fact (event).

One thing I’m still refining is how to handle versioning. When the CreateOrderPayload changes, you’ll likely need to version your events (e.g., OrderCreatedV2). Currently, I’m exploring whether template literal types—similar to how I handled TypeScript Template Literal Types for Type-Safe Pathing in Configs—can help automate the migration of event schemas. It’s a work in progress, and I’m still not 100% sure if the complexity is worth the benefit there.

FAQ

Why not just use classes? Classes can work, but they introduce runtime overhead and instanceof checks. Branded types are erased at runtime, so you get the safety without the extra code weight.

Does this affect performance? Not at all. TypeScript types are strictly for development time. Your production JavaScript stays exactly the same, which is exactly what we want.

How do I handle nested objects? The Immutable<T> mapped type I shared is recursive. It will traverse through nested objects and apply readonly to every single property, keeping your entire tree immutable.

Ultimately, TypeScript is about making your assumptions explicit. By using these patterns, you’re turning your domain rules into code that the compiler enforces for you. It won't catch every bug, but it will definitely stop the ones that keep you up at night.

Back to Blog

Similar Posts

TypeScriptJavaScriptJune 21, 20264 min read

TypeScript Value Objects: Eliminating Primitive Obsession in Your Code

TypeScript Value Objects help you eliminate primitive obsession by wrapping raw data in domain-specific types. Learn to prevent bugs with better type safety.

Read more
TypeScriptJavaScript
June 23, 2026
4 min read

TypeScript Branded Types for Preventing Silent Data Loss

TypeScript Branded Types help you prevent silent data loss by enforcing strict ID validation at compile-time, moving beyond simple primitive types.

Read more
TypeScriptJavaScriptJune 22, 20264 min read

TypeScript satisfies operator: Enforce API Contract Integrity

The TypeScript satisfies operator helps you build a type-safe API by validating object structures against interfaces without losing specific literal types.

Read more