Using Opaque Types To Solve Date Modelling/Formatting Problems

Nick Best
4 min readOct 16, 2019

--

Typed Functional Programming Languages

About a year ago, as a development team we made the decision to switch from writing our React-Native application in plain dynamically-typed JavaScript to using JavaScript with Flow. Tired of having to run code to find type errors, (or worse, having them show up in Sentry) we decided to start using Flow. Flow is not a new language; built by Facebook, it is a static type checker that sits on top of JavaScript with specifications for how typing should be implemented. While writing code, you can run a server that will parse all your files and tell you if you are making any type mistakes. Before your project gets built, Flow strips all the code that has to do with typing out and generates plain JS files. This makes integrating an untyped JS project with Flow easy, as you don’t have to retro-actively type all your old code if you don’t want to.

So far the switch to statically typed JS has been great for us. It has increased the correctness and clarity of our codebase. It gives us a few of the same great benefits of writing good unit tests: namely, ensuring the correctness of a piece of code and making clear the specifications of a piece of code. Let me give you an example of how static typing has done this for us. Through typing all of our application’s reducers (we use redux to manage state) there is automatic documentation about what state that reducer can and cannot store. The underlying structure of the reducer is apparent as soon as you open the file. This is great, as it is essentially “force documentation”. Gone are the days of pestering your co-worker to add some documentation to their reducer about the shape the reducer object should be taking. Gone are the days of having to read all the cod inside a reducer to figure out how you should structure the data inside your actions’ payloads.

dayjs().format(), dayjs().toISOString(), new Date(), or moment()???

As our codebase at Numida grew, we began to have an insidious little problem. Date formatting and date modelling became inconsistent. Having had multiple designers and developers come and go (and no formal agreement or documentation about date formatting and modelling), we were left with a codebase that used many different models and formats for dates in similar places. Moment.js, dayjs, and the native Date library were all used in the codebase in interchangeable places. Futhermore, we probably had about 15 different variations of a date format being rendered on screen by different components across the app. This caused me many headaches when writing code. Even worse, it caused the app itself to look inconsistent and unprofessional.

Restoring Order: Typing to the Rescue

The solution was not difficult. Reign in the chaos by routing all our date modelling/formatting through a utils file. I defined a limited set of date models and corresponding formats and gave them all descriptive names. As a product team, we decided on a finite set of date formats and corresponding definitions for where each format should be used.

before:

dayjs().format('DD-MM-YYYY');

after:

utils.formatMediumDate(utils.now());

Yay! Now, anywhere there is a representation of a date in the codebase, it comes along with a descriptive name. Furthermore, I won’t have to try to remember if the date should be formatted as DD-MM-YYYY or D-MM-YYYY.

To more strictly enforce this new solution, I wanted a way to type out the set of use-able dates that we decided upon. In Flow you cannot define types with more particularity than primitive types. E.g. I cannot specify a type of the form ‘xx-xx-xxxx’ where x is any character and the dashes are dashes. Opaque type aliasing helped me get around this problem.

Opaque Type Aliases

I wanted to differentiate the different types of date formats I was creating, but their underlying types were all strings. Using Opaque types I was able to hide that underlying type. Here is how that works:

In the defining file of my date type aliases, the opaque type alias behaves the same as the regular type alias:

Dayjs.format() returns a string type not a MediumDate type. Because this is the defining file of my opaque type alias, this code will not cause any typing errors. However if I were to write the following code outside of the defining file:

we would get a type error. This is because our stricter typing is not in effect and the function needs to return a MediumDate, not just a string. In the situation above the developer needs to import the utils function that returns a MediumDate and use that. The goes one step further in making sure our team is using the proper dating rules.

Conclusion

Strict typing is great. It will clean up your codebase, reduce typing/runtime errors, app crashes, and more. Your developers will love the increased simplicity and think up creative ways to use it to make your application better.

--

--