Hi folks! Some time ago, while browsing the latest TC39 proposals, I stumbled upon one that got me excited — and a little skeptical. It’s about partial application syntax for JavaScript. At first glance, it seems like the perfect fix for many common coding headaches, but as I thought it over, I realized there’s both a lot to like and some room for improvement.
Even better, these concerns sparked a whole new idea that could make JavaScript even more powerful. Let me take you on this journey, complete with realistic examples of how these features could change the way we code every day.
TLDR: the article come from my old issue to the proposal: https://github.com/tc39/proposal-partial-application/issues/53
Partial application lets you “preset” some arguments of a function, returning a new function for later use. Our current code looks like this:
const fetchWithAuth = (path: string) => fetch( { headers: { Authorization: "Bearer token" } }, path, ); fetchWithAuth("/users"); fetchWithAuth("/posts");
The proposal introduces a ~() syntax for this:
const fetchWithAuth = fetch~({ headers: { Authorization: "Bearer token" } }, ?); fetchWithAuth("/users"); fetchWithAuth("/posts");
See what’s happening? The fetchWithAuth function pre-fills the headers argument, so you only need to supply the URL. It’s like .bind() but more flexible and easier to read.
The proposal also allows you to use ? as a placeholder for unfilled arguments and ... for a rest parameter. For example:
const sendEmail = send~(user.email, ?, ...); sendEmail("Welcome!", "Hello and thanks for signing up!"); sendEmail("Reminder", "Don't forget to confirm your email.");
My favorite part is that I don't need to duplicate the type annotations!
Sounds useful, right? But there’s a lot more to unpack.
Let’s start with a practical pain point: function closures and stale variable references.
Say you’re scheduling some notification. You might write something like this:
function notify(state: { data?: Data }) { if (state.data) { setTimeout(() => alert(state.data), 1000) } }
Did you already see the problem? The "data" property might change during timeout and the alert will show nothing! Fixing this requires explicitly passing the value reference, hopefully, "setTimeout" accept additional arguments to pass it into the callback:
function notify(state: { data?: Data }) { if (state.data) { setTimeout((data) => alert(data), 1000, state.data) } }
Not bad, but it’s not widely supported across APIs. Partial application could make this pattern far more universal:
function notify(state: { data?: Data }) { if (state.data) { setTimeout(alert~(state.data), 1000) } }
By locking in state.data at the time of function creation, we avoid unexpected bugs due to stale references.
Another practical benefit of partial application is eliminating redundant work when processing large datasets.
For example, you have a mapping logic, which needs to calculate additional data for each iteration step:
const fetchWithAuth = (path: string) => fetch( { headers: { Authorization: "Bearer token" } }, path, ); fetchWithAuth("/users"); fetchWithAuth("/posts");
The problem is in proxy access to this.some.another, it is pretty heavy for calling each iteration step. It would be better to refactor this code like so:
const fetchWithAuth = fetch~({ headers: { Authorization: "Bearer token" } }, ?); fetchWithAuth("/users"); fetchWithAuth("/posts");
With partial application we can do it less verbose:
const sendEmail = send~(user.email, ?, ...); sendEmail("Welcome!", "Hello and thanks for signing up!"); sendEmail("Reminder", "Don't forget to confirm your email.");
By baking in shared computations, you make the code more concise and easier to follow, without sacrificing performance.
Now, here’s where I started scratching my head. While the proposed syntax is elegant, JavaScript already has a lot of operators. Especially the question mark operators ?. Adding ~() might make the language harder to learn and parse.
What if we could achieve the same functionality without introducing new syntax?
Imagine extending Function.prototype with a tie method:
function notify(state: { data?: Data }) { if (state.data) { setTimeout(() => alert(state.data), 1000) } }
It’s a bit more verbose but avoids introducing an entirely new operator. Using an additional special symbol for placeholders we can replace the question mark.
function notify(state: { data?: Data }) { if (state.data) { setTimeout((data) => alert(data), 1000, state.data) } }
It is perfectly polypiling without additional built-time complexity!
function notify(state: { data?: Data }) { if (state.data) { setTimeout(alert~(state.data), 1000) } }
But this is only the top of the iceberg. it makes the placeholder concept reusable across different APIs.
Here’s where things get really interesting. What if we expanded the symbol concept to enable lazy operations?
Suppose you’re processing a list of products for an e-commerce site. You want to show only discounted items, with their prices rounded. Normally, you’d write this:
class Store { data: { list: [], some: { another: 42 } } get computedList() { return this.list.map((el) => computeElement(el, this.some.another)) } contructor() { makeAutoObservable(this) } }
But this requires iterating over the array twice. With lazy operations, we could combine both steps into one pass:
class Store { data: { list: [], some: { another: 42 } } get computedList() { const { another } = this.some return this.list.map((el) => computeElement(el, another)) } contructor() { makeAutoObservable(this) } }
The Symbol.skip tells the engine to exclude items from the final array, making the operation both efficient and expressive!
Imagine calculating the total revenue from the first five sales. Normally, you’d use a conditional inside .reduce():
class Store { data: { list: [], some: { another: 42 } } get computedList() { return this.list.map(computeElement~(?, this.some.another)) } contructor() { makeAutoObservable(this) } }
This works, but it still processes every item in the array. With lazy reductions, we could signal early termination:
function notify(state: { data?: Data }) { if (state.data) { setTimeout(alert.tie(state.data), 1000) } }
The presence of Symbol.skip could tell the engine to stop iterating as soon as the condition is met, saving precious cycles.
These ideas — partial application, referential transparency, and lazy operations — aren’t just academic concepts. They solve real-world problems:
Whether we stick with ~() or explore alternatives like tie and Symbol.skip, the underlying principles have enormous potential to level up how we write JavaScript.
I vote for the symbol approach as it is easy to polyfill and has various uses.
I’m curious—what do you think? Is ~() the right direction, or should we explore method-based approaches? And how would lazy operations impact your workflow? Let’s discuss in the comments!
The beauty of JavaScript lies in its community-driven evolution. By sharing and debating ideas, we can shape a language that works better for everyone. Let’s keep the conversation going!
The above is the detailed content of Rethinking JavaScript. Partial Application, Referential Transparency, and Lazy Operations. For more information, please follow other related articles on the PHP Chinese website!