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
The Proposal
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.
The Case for Referential Transparency
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.
Reducing Repeated Computations
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:
class Store {
data: { list: [], some: { another: 42 } }
get computedList() {
return this.list.map((el) => computeElement(el, this.some.another))
}
contructor() {
makeAutoObservable(this)
}
}
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:
class Store {
data: { list: [], some: { another: 42 } }
get computedList() {
const { another } = this.some
return this.list.map((el) => computeElement(el, another))
}
contructor() {
makeAutoObservable(this)
}
}
With partial application we can do it less verbose:
class Store {
data: { list: [], some: { another: 42 } }
get computedList() {
return this.list.map(computeElement~(?, this.some.another))
}
contructor() {
makeAutoObservable(this)
}
}
By baking in shared computations, you make the code more concise and easier to follow, without sacrificing performance.
Why Add New Syntax?
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?
A Method-Based Alternative
Imagine extending Function.prototype
with a tie
method:
function notify(state: { data?: Data }) {
if (state.data) {
setTimeout(alert.tie(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.
const fetchWithAuth = fetch.tie(
{ headers: { Authorization: "Bearer token" } },
Symbol.skip
);
fetchWithAuth("/users");
fetchWithAuth("/posts");
It is perfectly polypiling without additional built-time complexity!
class Store {
data: { list: [], some: { another: 42 } }
get computedList() {
return this.list.map(computeElement.tie(Symbol.skip, this.some.another))
}
contructor() {
makeAutoObservable(this)
}
}
But this is only the top of the iceberg. it makes the placeholder concept reusable across different APIs.
Lazy Operations: Taking It Further
Here’s where things get really interesting. What if we expanded the symbol concept to enable lazy operations?
Example 1: Combining .filter()
and .map()
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:
const discountedPrices = products
.filter(p => p.discount > 0)
.map(p => ({ price: Math.round(p.price * (1 - p.discount)) }));
But this requires iterating over the array twice. With lazy operations, we could combine both steps into one pass:
const discountedPrices = products.map(p =>
p.discount > 0
? { name: p.name, price: Math.round(p.price * (1 - p.discount)) }
: Symbol.skip
);
The Symbol.skip
tells the engine to exclude items from the final array, making the operation both efficient and expressive!
Example 2: Early Termination in .reduce()
Imagine calculating the total revenue from the first five sales. Normally, you’d use a conditional inside .reduce()
:
const revenue = sales.reduce((acc, sale, i) =>
i < 5 ? acc + sale.price : acc,
0
);
This works, but it still processes every item in the array. With lazy reductions, we could signal early termination:
const revenue = sales.reduce((acc, sale, i) =>
i < 5 ? acc + sale.price : Symbol.skip,
0
);
The presence of Symbol.skip
could tell the engine to stop iterating as soon as the condition is met, saving precious cycles.
Why This Matters
These ideas — partial application, referential transparency, and lazy operations — aren’t just academic concepts. They solve real-world problems:
- Cleaner API usage: Lock arguments upfront and avoid stale references.
- Improved performance: Eliminate redundant computations and enable more efficient iteration.
- Greater expressiveness: Write concise, declarative code that’s easier to read and maintain.
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.
What’s Next?
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!
Top comments (0)