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.
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.
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:
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!)
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:
myobject::myfunction()
into myobject::a()
, given myfunction
was minified to a
.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.
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 number
s and not on array of string
s? That’s totally possible and not a real issue at all.
Because, you know, it is just a function.
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.
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: