1. Resources
  2. /
  3. Blog
  4. /
  5. Making Magick with JavaScript ✨

Making Magick with JavaScript ✨

4 minute read

As someone who’s spent a lot of time working with JavaScript’s rather desolate built-in functionality, something I found quite nice in Ruby (or, more accurately, Rails) is ActiveSupport’s Numeric extensions. The 3.hours.ago syntax ActiveSupport enables is more English-like than, for instance, Moment.js

1
moment().subtract(3, 'hours')

Unfortunately, this approach involves monkey patching the standard library, which I’ve never been a fan of as it opens you up to unexpected behaviour. Some people are also doing this in JavaScript, but never felt comfortable with the idea of tacking such things onto the built-in types.

After writing several Pull Requests adjusting the delay in a setTimeout, I sought better way. Sure, I could just define a const elsewhere with a nice descriptive name like REFRESH_INTERVAL_MILLISECONDS, but that’s overly verbose and creates a disconnect between the use and the definition which makes it less desirable.

I’m always curious about new and different ways of using the programming languages at my disposal, so I started tinkering!

Approach #1: Messing with Tagged Template Literals

The first thing that came to mind was an ES6 feature I’d been fiddling with at the time: tagged template literals — basically, modern JavaScript’s answer to printf.

When a template literal (the technical term for those new ES6-style string literals `surrounded by backticks`) is tagged with a function, the function is called with an array of string values and each interpolation as an additional argument, both derived from the template literal. From here, you can manipulate or react to each piece of the string, and return whatever you want! If you‘d like to know more about template literals, Max Stoiber has written a fantastic introduction.

Screenshot of a code example: "span`${10} seconds`.in.milliseconds"

Your number and time unit are included within the template literal. The tag function will then return an object whose properties contain your number in each time unit Moment.js supports.

Admittedly, this is not a very reasonable way to use template literal tags, but it worked.

Approach #2: Calling functions in context with Babel

Dissatisfied with both the overly verbose syntax and weird use of template literal tags, I went looking for more things you could do with ES6 and modern compilers.

It turned out that Babel, which we were already using to build Buildkite, includes some experimental language features. It was here I found an experimental shorthand function bind syntax.

Though not the intended use, it allows you to pass a value into a function from before the function name. Within the function, it’s available as the this variable! Using the bind shorthand, I could then prepend a normal Number object, have it passed into my functions, and avoid monkey patching entirely! 🙌🏼

Screenshot of a code example: “2::seconds.in(milliseconds)”

An expression in the format 2::seconds.in(milliseconds) gets turned into seconds.in.call(2, milliseconds) at compile time, making it nice and readable, but providing the browser with standard JavaScript code.

This was a fantastic hack, and I was sort of amazed that I could bend JavaScript to my will like this!

A few days later, when I was showing my little hack off to Glen Maddern, he pointed out it could go one step further further…

Approach #3: The birth of Metrick

The function bind shorthand can either call your function if arguments are provided, or it can just bind. These are both provided by the Function prototype. In this case, I’m using call on the in function of whatever unit I’m converting from.

It turns out that Babel’s function bind syntax doesn’t actually care what the object you’re binding is. Because of this, you can provide your own bind function, and include whatever functionality you want.

I added a bind function which returns milliseconds, and suddenly…

Screenshot of a code example: “15::seconds”

…the syntax was even shorter, and even cleaner!

Metrick: literally one weird trick

It was a combination of approaches 2 and 3 that I ultimately published in my npm package, Metrick. I created a duration module which supported times from milliseconds to years, support for data, temperature and length units later on.

Metrick has classes for each type of unit. Metrick will only convert units between instances of the same class: you can’t convert an hour into celsius, as celsius is an instance of Temperature rather than Duration.

To keep things consistent, each unit class extends a base Unit class. This base class provides the actual functionality; it’s responsible for providing the in and bind functions, as well as performing the actual unit conversion.

The advantage of this structure is that it makes Metrick very flexible!

You can use the built-in units, or you can specify your own! You can even go so far as to create new types of unit if you need to operate on something niche.

Languages aren’t static

People are still trying to build on JavaScript’s strengths, to improve on its weaknesses, and to bring to it new ideas, and that’s really exciting.

Taking my frustration at magic numbers and applying that to discovering something neat has taught me that programming languages (in particular JavaScript) are more flexible than I’d thought.

I think it’s really worth exploring outside the norm, and I had a lot of fun making Metrick! Our tools and languages are often more flexible than we think.


Related posts

Start turning complexity into an advantage

Create an account to get started with a 30-day free trial. No credit card required.

Buildkite Pipelines

Platform

  1. Pipelines
  2. Pipeline templates
  3. Public pipelines
  4. Test Engine
  5. Package Registries
  6. Mobile Delivery Cloud
  7. Pricing

Hosting options

  1. Self-hosted agents
  2. Mac hosted agents
  3. Linux hosted agents

Resources

  1. Docs
  2. Blog
  3. Changelog
  4. Webinars
  5. Plugins
  6. Case studies
  7. Events

Company

  1. About
  2. Careers
  3. Press
  4. Brand assets
  5. Contact

Solutions

  1. Replace Jenkins
  2. Workflows for AI/ML
  3. Testing at scale
  4. Monorepo delivery

Support

  1. System status
  2. Forum