DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
  1. DZone
  2. Coding
  3. JavaScript
  4. Type Safe Message Dispatch in TypeScript

Type Safe Message Dispatch in TypeScript

Follow along as one developer shows us how to create type safe code using the TypeScrip language.

David Karapetyan user avatar by
David Karapetyan
·
Dec. 04, 18 · Tutorial
Like (3)
Save
Tweet
Share
9.12K Views

Join the DZone community and get the full member experience.

Join For Free

TypeScript continues to be amazing. Anders and team are doing an incredible job making the language accessible and at the same time powerful enough to express interesting invariants that can be encoded with conditional and mapping types.

I'm currently working on a workflow toolkit and building it in TypeScript has allowed me to express the message dispatch logic in a type safe way. Putting the pieces together has been a lot of fun so I'm going to outline the pattern in case others find it useful.

The skeleton for a message dispatch loop consists of something that looks like the following:

function dispatch(message, handlers) {
  const messageType = message.type;
  if (handlers[messageType]) {
    return handlers[messageType](message);
  } else {
    throw new Error(`Unknown message type ${message.type}: ${message}`);
  }
}

Without types there isn't much we can say about this function. All we know is that there is some kind of mapping from types of message to functions that take the message and do something. We want to preserve as much of this structure as possible while at the same time adding as much type safety as possible. Fortunately, TypeScript has all the ingredients to make this possible.

First thing we need to do is specify the message types and give the compiler enough information to help us out:

type Eq<A, B> = [A] extends [B] ? ([B] extends [A] : true : never) : never;

type MessageType = 'work' | 'exit';
interface M<T extends MessageType, P> = {
  type: T
  payload: P
}
type WorkMessage = M<'work', any>;
type ExitMessage = M<'exit', any>;
type Message = WorkMessage | ExitMessage;
{ const _: Eq<MessageType, Message["type"]> = true; }

In a production environment you would spell out all the details and not leave the payload type unspecified but for demonstration purposes I'm going to leave the payload unspecified because it is easy to extend the pattern to typed payloads. The other thing I need to explain is Eq.

What's going on there is that we are verifying that every message type actually has a message assigned to it.  Eq will show up throughout so make sure you understand what is going on. If the types in Eq are not equal then we will not be able to assign true to it and the compiler will tell us the assignment in the block is not possible.

Now that the message type is specified we can use a switch statement and ask the compiler to help us out with the dispatch function:

function dispatch(message: Message, handlers) {
  switch (m.type) {
    case 'work': {
      return handlers[m.type](m);
    }
    case 'exit': {
      return handlers[m.type](m);
    }
    default: {
     const n: never = m;
     throw new Error(`This can never happen: ${n}`);
    }
  }
}

Thanks to TypeScript's exhaustive checking, the compiler will complain if we forget to check for all the message types that are in MessageType. The assignment in the default block will have red squiggly lines and it will tell us that never is not assignable to some type we forgot to check so when we try to compile we will get warnings about missing cases.

The messages are now handled properly but handlers is untyped. We can assign a type to it as well by using conditional and mapping types. First thing we need to do is specify the type of the handler function:

interface Mapping {
  work: WorkMessage
  exit: ExitMessage
}

type HandlerFunction<K> =
  K extends MessageType ? (m: Mapping[K]) => void : never;
{
  const _: Eq<keyof Mapping, MessageType> = true;
  const __: { [K in keyof Mapping]: Eq<K, Mapping[K]["type"]> } = {
    work: true,
    exit: true
  };
}
type HandlerMapping = { [K in keyof Mapping]: HandlerFunction<K> };

We are again using the same trick with Eq to verify certain invariants. We want to make sure that the key for the mapping and the actual type of the message match up. If they don't match up then the assignment will fail during compile time and we will get an error/warning that will tell us something is wrong. So if we tried to assign the key exit to the message WorkMessage the assignment would be incorrect and we'd get a warning/error at compile time.

Now with all those types we can make sure handlers variable in our dispatch function is also correct:

function dispatch(message: Message, handlers: HandlerMapping) {
  switch (m.type) {
    case 'work': {
      return handlers[m.type](m);
    }
    case 'exit': {
      return handlers[m.type](m);
    }
    default: {
     const n: never = m;
     throw new Error(`This can never happen: ${n}`);
    }
  }
}

This makes everything type safe. If you know of a better way to structure the dispatch logic and types then let me know. I'm pretty happy with this solution but open to a better design. I kind wish I could abstract the pattern further but I'm pretty sure that requires higher order types. There are a few simplification to be made but I'll leave that as an exercise for the reader.

TypeScript

Published at DZone with permission of David Karapetyan, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • What Is Policy-as-Code? An Introduction to Open Policy Agent
  • The 31 Flavors of Data Lineage and Why Vanilla Doesn’t Cut It
  • Core Machine Learning Metrics
  • Apache Kafka vs. Memphis.dev

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: