Why We Want Pattern-Matching in JavaScript

A worked example, showing how much it can clarify code.

September 23, 2018 (updated September 24, 2018)Filed under tech#javascript#programming languagesMarkdown source

I’ve often noted how much I want the JavaScript pattern-matching proposal to land. I noted in conversation with some people recently, though, that it’s not always obvious why it will be so helpful. Similarly, Dave Herman recently noted to me that DHH’s mantra of “Show me the code” is a really helpful tool for thinking about language design. (I tend to agree!) So with that in mind, here’s a piece of example code from the Ember app I work on today, very slightly modified to get at the pure essentials of this particular example.1

The context is a UI component which shows the user their current discount, if any, and provides some nice interactivity if they try to switch to a different discount.

First, some types that we’ll use in the example, which I use in the actual component to avoid the problems that inevitably come with using string values for these kinds of things. Linters like ESLint or type systems like TypeScript or Flow will catch typos this way, and you’ll also get better errors at runtime even if you’re not using a linter or a type system!2

const DiscountTypes = {
  Offer: 'Offer',
  Coupon: 'Coupon',
  None: 'None',
};

const Change = {
  OfferToOffer: 'OfferToOffer',
  OfferToCoupon: 'OfferToCoupon',
  CouponToCoupon: 'CouponToCoupon',
  CouponToOffer: 'CouponToOffer',
};

Now, we set up a component which has a little bit of internal state to track the desired change before we submit it, which we display differently based on what the value of the ES5 getter for change is here:

class DiscountComponent {
  constructor(currentDiscountType) {
    this.currentDiscountType = currentDiscountType;
    this.newDiscountType = null;
  }

  changeDiscount(newDiscountType) {
    this.newDiscountType = newDiscountType;
  }

  submitChange() {
    // logic for talking to the server
  }

  get change() {
    const { currentDiscountType, newDiscountType } = this;

    if (currentDiscountType === DiscountTypes.Offer) {
      if (newDiscountType === DiscountTypes.Offer) {
        return Change.OfferToOffer;
      } else if (newDiscountType === DiscountTypes.Coupon) {
        return Change.OfferToCoupon;
      } else if (newDiscountType === DiscountTypes.None) {
        return null;
      } else {
        assertInDev(
          `Missed a condition: ${currentDiscountType}, ${newDiscountType}`
        );
      }
    } else if (currentDiscountType === DiscountTypes.Coupon) {
      if (newDiscountType === DiscountTypes.Offer) {
        return Change.CouponToOffer;
      } else if (newDiscountType === DiscountTypes.Coupon) {
        return Change.CouponToCoupon;
      } else if (newDiscountType === DiscountTypes.None) {
        return null;
      } else {
        assertInDev(
          `Missed a condition: ${currentDiscountType}, ${newDiscountType}`
        );
      }
    } else if (currentDiscountType === DiscountTypes.None) {
      return null;
    } else {
      assertInDev(
        `Missed a condition: ${currentDiscountType}, ${newDiscountType}`
      );
    }
  }
}

Here’s the exact same semantics for computing the change value we’re interested, but with pattern matching:

class DiscountComponent {
  // ...snip

  get change() {
    case ([this.currentDiscountType, this.newDiscountType]) {
      when [DiscountTypes.Offer, DiscountTypes.Offer] ->
        return Change.OfferToOffer;
      when [DiscountTypes.Offer, DiscountTypes.Coupon] ->
        return Change.OfferToCoupon;
      when [DiscountTypes.Coupon, DiscountTypes.Offer] ->
        return Change.CouponToOffer;
      when [DiscountTypes.Coupon, DiscountTypes.Coupon] ->
        return Change.CouponToCoupon;
      when [DiscountTypes.None, ...] || [..., DiscountTypes.None] ->
        return null;
      when [...] ->
        assertInDev(
          `Missed a condition: ${currentDiscountType}, ${newDiscountType}`
        );
    }
  }
}

The difference is stark. It’s not just that there are fewer lines of code, it’s that the actual intent of the code is dramatically clearer. (And while I’ve formatted it for nice display here, those are all one-liners in my normal 100-characters-per-line formatting.)

My preference would be for pattern-matching to have expression semantics, so you wouldn’t need all the return statements in the mix—and it’s possible, depending on how a number of proposals in flight right now shake out, that it still will. Even if pattern matching doesn’t ultimately end up with an expression-based syntax, though, we can still get a lot of those niceties if the do-expression proposal lands:

class DiscountComponent {
  // ...snip

  get change() {
    return do {
      case ([this.currentDiscountType, this.newDiscountType]) {
        when [DiscountTypes.Offer, DiscountTypes.Offer] ->
          Change.OfferToOffer;
        when [DiscountTypes.Offer, DiscountTypes.Coupon] ->
          Change.OfferToCoupon;
        when [DiscountTypes.Coupon, DiscountTypes.Offer] ->
          Change.CouponToOffer;
        when [DiscountTypes.Coupon, DiscountTypes.Coupon] ->
          Change.CouponToCoupon;
        when [DiscountTypes.None, ...] || [..., DiscountTypes.None] ->
          null;
        when [...] ->
          assertInDev(
            `Missed a condition: ${currentDiscountType}, ${newDiscountType}`
          );
      }
    }
  }
}

Again, this is profoundly clearer about the intent of the code, and it’s far easier to be sure you haven’t missed a case.3

Edit: after some comments on Twitter, I thought I’d note how this is even nicer in pure functions. If we assume that it gets expression semantics (which, again, I’m hoping for), a pure functional version of the sample above would look like this:

const change = (currentType, newType) =>
  case ([currentType, newType]) {
    when [DiscountTypes.Offer, DiscountTypes.Offer] ->
      Change.OfferToOffer;
    when [DiscountTypes.Offer, DiscountTypes.Coupon] ->
      Change.OfferToCoupon;
    when [DiscountTypes.Coupon, DiscountTypes.Offer] ->
      Change.CouponToOffer;
    when [DiscountTypes.Coupon, DiscountTypes.Coupon] ->
      Change.CouponToCoupon;
    when [DiscountTypes.None, ...] || [..., DiscountTypes.None] ->
      null;
    when [...] ->
      assertInDev(
        `Missed a condition: ${currentDiscountType}, ${newDiscountType}`
      );
  };

This may not be quite as clear as the same thing in F or Elm or another language in that family… but it’s amazingly better than anything we’ve seen in JavaScript to date.


  1. assertInDev looks a little different; we’re actually using the Maybe type from my True Myth library instead of returning null; it’s an Ember app; as such it uses a @computed decorator; and of course it’s all in TypeScript. I chose to write it with standard JavaScript to minimize the number of things you have to parse as a reader.

  2. In the actual TypeScript, these are defined with an enum.

  3. Fun fact: the original code actually had missed a number of cases, which I learned only because TypeScript’s strictNullChecks setting informed me.