Custom JavaScript Flavors

Two things I wish existed in JavaScript’s syntax, and probably will never exist.

.

I'm using JavaScript (and TypeScript) as my main language for a long time, but I also experiment with other languages. Therefore, I started to build my own taste and preference on the syntax and I think we can make it better. Some ideas are already tracked in TC39 but some are not.

There’s a hacky demo of these suggestions in this ASTExplorer playground, so you can play with it and see how it generates the code.

Postfix await

When I first saw the postfix await operator in Rust, I was very confused. It looked very weird. After a while, it became much clearer to me that it makes reading code much easier to me.

It makes sense: when I’m reading English, I expect the text to be from left to right (not in Hebrew though, but whatever). Code is usually written in English and from left to right too. There are some exceptions to that: prefix operators.

I think these should be avoided as they’re harder to follow. I’m talking specifically on await here, but I think that a .not() function would be great for booleans.

To understand the confusion and why postfix operators are better for these cases, let’s look at some code.

Imagine that we have a calculator library that we’re willing to use, and we’re given with the following code:

const value = number(0).add(5).add(3).divide(2)

In my opinion, this code is relatively easy to read and understand: the dot-notation is very clear—we can read it from left to right and it makes total sense. What would happen if the API changed into an asynchronous one as if we were running the calculator in a different thread/worker?

If the API were async, each function call would be prefixed with await to keep the logic the same:

const value = await (await (await (await number(0)).add(5)).add(3)).divide(2)

How’s that code compared to the previous one? Not so good, right? A good way to overcome this is by using promise.then(...) but that’s cheating as we stop using the prefix operator and opt for a postfix function call. Another option would be to break this code into multiple lines, even when really unnecessary as it’s not a complicated calculation.

So, how can we fix that? Well, what if await was a postfix operator and worked like an attribute/property that is accessible on any object?

// before
const value = number(0).add(5).add(3).divide(2);

// prefix await
const value = await (await (await (await number(0)).add(5)).add(3)).divide(2);

// postfix await
const value = number(0).await.add(5).await.add(3).await.divide(2).await;

Having an obj.await instead of await obj allows us to chain the calls and have a clear left-to-right flow that is easy to read, easy to write, and easy to update when necessary.

The bind operator ::

This is a TC39 proposal that did not take off. Its premise is to allow calling a function while binding this to the left hand operand. So: something::myFunction() will be compiled and treated just like myFunction.call(something). You might ask yourself, why is this any good?

Combining data with behavior is the basis of OOP, or Object Oriented Programming. JavaScript is not full OOP language—but it is something in the middle. The idea here is to have all the pros of simple free-function calls, combined with the dot-notation syntax:

Dot-notation is easier to read

See the following examples:

// #1
concatWith(" world!", capitalize(trim(greeting)))

// #2
greeting::trim()::capitalize()::concatWith(" world!")

Which one is easier to read? The bind operator allows seeing the data flow from left to right. The bind operator looks like the dot notation, with two colons instead of a single dot. This brings a very similar behavior and expectation, while still being easy on the eyes, compared to the pipeline operator (which is also good!)

Free functions are tree-shakable and minifiable

Unlike methods on classes and objects, free functions are easily tree-shakeable, easily minified, and allow dead code elimination. That means that if you never call myHugeFunction(user), then myHugeFunction and its dependencies can be removed from the bundle safely, resulting in less code sent to the user. What a win:

  • You don’t reference them by name, but through a reference that can be statically analyzable. This allows minifying myobject::myfunction() into myobject::a(), given myfunction was minified to a.
  • We can statically know whether a function was used from an import, making it easier to follow usages for properly tree-shaking what we need, for smaller production artifacts.

Free functions are better for modularity

Many JavaScript applications use a standard library extensions to add missing functionality in the JS engines and normalize their behavior.

Older standard library extensions used to monkey patch the browser's classes, like Array and String, to provide methods like Array#map and String#capitalize, etc, so they were used like "hello".capitalize().

lodash and other modern libraries however, are a set of functions that can work against the data structures as input, instead of adding it to the original class prototype. So the previous example would be capitalize("hello"). This is both non-destructive for the prototype, and enables all the tree-shaking described in the previous section.

That also means anyone can create their own functions that operate on such data structures. Maybe array::groupByIntoMap(value => key) that groups a set of values into a map? Maybe an iter::map(v => v) function that operates on any iterator and not just arrays?

This is a powerful feature, because we can encapsulate this logic in modules, ship them, and share them with other projects. People can keep extending them, with no need of surgical interference or monkey-patching.

It’s fairly easier to type using TypeScript

Not having to use TypeScript’s Augmentation feature is a big win here. It leaves you with implementing your function, which can allow arbitrary generics. What if you want to support a function that only operates on array of numbers and not on array of strings? That’s totally possible and not a real issue at all.

Because, you know, it is just a function.

Easier to grasp over pipeline operator

The proposed pipeline operator (|>) can solve most of these issues, and frankly, I'm excited for it. However, I think that newcomers will feel more at home with the bind operator, because it fits the dot notation over "function composition" which might be intimidating for them.

I really hope that in the near future, the pipeline and/or bind operators will be stable.

🚮 Unfortunately, I do not use these syntax extensions

Thanks to tools like Babel and SWC, every developer can enrich JavaScript’s syntax and enable whacky transformations. I used to apply all TC39 Stage 0 proposals back in 2014-2015 because I wanted to experiment with modern JavaScript.

Nowadays, much like jailbreaking iPhone or using jQuery: these settings are not really needed as JavaScript is modern: the most important features I needed when I enabled stage 0 proposals are already baked in, and TC39 is updating the spec frequently, so JavaScript is not stagnating.

So why ain’t I using SWC or Babel to extend my JavaScript syntax? This boils down to two answers:

  • I do not like custom solutions, and rather use industry standard to make sure everyone can contribute to my code with ease, including the future me, which might forget about these code transformations.
  • The code might be able to run, but in my opinion, it worths nothing if TypeScript, Prettier or other utilities and static analysis tools are broken. They are much more important than making my code a bit nicer to my own eyes.
Thanks to Anthony Shew