Optional named parameters

Hello. I would like to propose the idea of optional named parameters for functions.

Motivation

As of now, developers have to remember and memorise the order of occurance function arguments to correctly use any function.
The other alternative is to pass objects, and one might easily pass in an object with a key value pair which the function does not need, or might mutate the values which should not be mutated.

Designing functions would also become easier since the developer has to worry less about order in which optional parameters are ordered from the left.

Proposal

I propose the language syntax and semantic to support optional named parameters. The following example should make it clear:

function greetPerson(first, last = '') {
  let message = `Hello, ${first}.`;
  if(last) {
    message+=` ${last}`;  
  }
  return message;
}

greetPerson('John', 'Doe') //? "Hello, John Doe"
greetPerson(last := 'Smith', first := 'Sam') //? "Hello, Sam Smith"

Other languages where this feature is available

This is a feature which is available in other programming languages like Python.

Rationale

This feature possesses the advantage of not worrying where the default parameter is mentioned in the function definition, for example: function person(first, last='', age) {...}.

I chose the above example because not all people have a last name. However, if one has to think of the possible applications, it can be quite useful: eliminating the need to memorise the order in which a function's parameters are defined.

One can clearly see the advantages of this when they are using a function which have more than a couple of parameters, as this does not warrant them to know the order of parameters. If they know the name of parameters, that should suffice.

Also, since it is optional, those who do not want to know the name of parameters and instead want to rely on the order of parameters, it works perfectly fine and is completely backwards compatible.

In case if a function has more than one parameters with default value, this feature also eliminates the overhead of thinking and evaluating which parameters the designer should put the rightmost, which one on the left.
For example: function personDetails(first, last='', age, children = []){...}.

Right now, the closest we have to this feature is to pass an object. That object can either be destructured in the function parameter, or later in the function definition.
However, often times the object can contain variables which the function does not need, and might mutate, which can be prevented if this feature were to exist.

To avoid that problem, one needs to create a new object, which might as well use named parameters. Another disadvantage of using the object as the parameter is unnecessary type declaration in another implementation of ECMAScript, namely TypeScript. And simpler parameters looks like an advandatge.

Without using the object, one can clearly see what are the parameters in a function, and use it directly, and also avoid unnecessary memorisation of the order of parameter.

I think "optional named parameters" is wonderful addition to the language. I welcome the opportunity to discuss about this idea.

Thank you for taking the time to read this proposal.

4 Likes

I don't think this change would be possible, since the syntax is already valid (assignment expressions):

let b;
console.log(b = 3); // 3
console.log(b);     // 3
1 Like

Updated the notation to:

greetPerson(last := 'Smith', first := 'Sam') //? "Hello, Sam Smith"

As is, this would make renaming a function argument a breaking change by default. I'd think we'd need to have a way for a function to opt itself in to this behavior - what would that look like?

I am not sure why this would be a breaking change. Perhaps I might be unsure of the mechanism to implement such design.

If that is the case, maybe we could make use of a keyword in front of the function, like how we have async, or symbol like * for generators, to distinguish the function.

What about enabling "call-by-object" notation instead:

Call-by-object is this:

func {  last : 'Smith', first : 'Sam'  };

and that is just a short form of

func ({  last : 'Smith', first : 'Sam'  });

where func is a function taking one argument.

Call-by-object notation is supported by script in Sciter, and example

But would this be optional, and allow the default order as well?

Hello @acagastya,

Personally, I think this is unnecessary as we could just pass an object as the argument.

Example with default properties...

function plot({ x = 0, y = 0 }) {
  return y - x;
}
plot({ x: 1, y: 1 });

For JSDocs...

/**
 * @param {{ x: number, y: number }} coordinates
 * @returns {number}
 */
1 Like

I agree with @ethanlal04. I use destructuring + construction because it's clearer and order-independent. I think that's what you need, right?

const a = ({ x=0, y=0 }) => console.log( x,y );

a({ x:99 }); //> 99, 0
a({ y:99 }); //> 0, 99
a({ y:88, x:99 }); //> 99, 88
1 Like

I think this should be syntactic sugar over already existing default parameters definition. Default parameters calls should complement default parameters definition.

So e.g. to call this:

function decrease(a, b = 1) {
  return a - b;
}

You could pass params in two ways:

// current
decrease(1,2); // 1-2 = -1
// new
decrease(a:1,b:2); // 1-2 = -1
decrease(b:2,a:1); // 1-2 = -1
decrease(1,b:2); // 1-2 = -1

So that would be similar to what Python allows. In Python, you can pass arguments by name, which I find more readable and convenient, and I don't have to change function declarations to use this. For example:

class MyDate {
	constructor(year, month, day=1, hours=0, minutes=0, seconds=0, ms=0) {
		this.dt = new Date(year, month-1, day, hours, minutes, seconds, ms);
	}
	// ...
}
let dt1 = new MyDate(year:2024, month:1, day:1, ms:100);
let dt2 = new MyDate(year:2024, month:1, day:1, ms:123);
dt2.elapsed(dt1) // 23 ms

Note that function declarations would stay the same, so you could use this new syntactic sugar with existing libraries. Libraries would still be ES6, but your code would work with the new JS/ES. So I think this is a good way to improve things (improve dev UX at higher levels).

And yes, changing a function would break calls. But that can be a good thing!

// suppose you have a function like that:
{
	let formatPrice = function (amount, currency = 'USD')
	  return `${amount} ${currency}`;
	}
	// this works
	let poundsAmount = formatPrice(amount, currency: 'GBP')
}

// and you later change it to:
{
	let formatPrice = function(amount, locale = new UserLocale('us'))
	  return `${locale.format(amount)} ${locale.currency}`;
	}
	// this would fail in linters as it should! (we want pounds here, not dollars)
	let poundsAmount = formatPrice(amount, currency: 'GBP')
}

Additionally, we could also add more syntactic sugar to sweeten the deal ;)

{
  let a=2;
  let b=3;
  decrease(a:a,b:b); // 2-3 = -1
  decrease(a:,b:); // 2-3 = -1
  decrease(2,b:); // 2-3 = -1
}

Above syntax is used in Ruby: Ruby 3.1.0 Released | Ruby.

Bonus: it already works well with a syntax highlighter here :)

Existing technics for declaring default params:

PS: Note that if you compile from TypeScript or Babel and resulting code has renamed params the proposal should still work. E.g. the transpiler sees:

class MyDate {
	constructor(year, month, day=1, hours=0, minutes=0, seconds=0, ms=0) {
		this.dt = new Date(year, month-1, day, hours, minutes, seconds, ms);
	}
	// ...
}

And also sees this call:

let dt1 = new MyDate(year:2024, month:1, day:1, ms:100);

It then transpiles the call to:

let dt1 = new MyDate(2024, 1, 1, undefined, undefined, undefined, /*ms:*/100);

So in C++ terms – the transpiler only need to know headers of libraries, not the code. And in generated code the param names no longer matter.

I think there needs to be an opt-in at the declaration.

As this is a new feature. Code as written today was not written aware that its argument names might become part of the public API.

It would mean that changing the function argument names is a breaking change, but the code never committed to those names as this feature did not exist when the code was authored.

I think breaking locality of transpilation is a huge deal. Probably a dealbreaker.

What I mean is that we should know how to transpile a function call without needing to know what function is being called. It may not even be possible to know what function is being called at compile time!!!!

I don't think TypeScript is compiled locally. Types are used to lint and resolve stuff. Some types are already published e.g. npm i @types/jquery. At a glance definitions like that seem more than enough to transpile any jQuery call with optional params/args.

You're proposing a JS feature, and JS features must work without TS. That said, even if you could lean on TS for the type info the TS team isn't even accepting new features which rely on having the complete type graph in order to make basic decisions about what code to emit.

It doesn’t need TS to work. I only mentioned it because it’s popular on npm and could be reused here to create a transpiler more quickly (I would hope).

If you have modules and load your classes as modules you don't need TS. You should be able to resolve everything in vanilla ES6 with the already loaded dependencies. I mean, you would probably have something like this in your <scrript type=module>:

import { MyDate } from "datetime.js"
let dt1 = new MyDate(year:2024, month:1, day:1, ms:100);

The uncompiled definition of MyDate is everything you need to resolve the parameters in the example above.

TypeScript definitions are just a one way you could simply use in a TS transpiler to generate final JS that is ES6 compatible. You could also do the same thing with Babel and use uncompiled code from node_modules to transpile your libraries.

I haven’t written a big parser in a while, but I’d guess you could optimize things by generating some header files for modules (similar to how people use tools to generate .d.ts files and publish them on npm). So mabe node could generate something like jsh files that only define types. For example, datetime.jsh could be:

class MyDate {
	constructor(year, month, day=1, hours=0, minutes=0, seconds=0, ms=0);
	elspased(dt);
}

And then publish this to npm. Obviously, this could be used for more than just resolving parameters/arguments (e.g. for static code analysis, dependency resolution, etc.). So much like .d.ts files are used now.

let ab = (a, b) => a;
let ba = (b, a) => a;

let fn = Math.random() > 0.5 ? ab : ba;

fn(a := true, b := false);

now what

I see you like it hard, OK :). Firstly it would just work if you have the assosciated defition of the function. We already got the __proto__ we could have __header__ or like we have name we could have argMap.

But I understand you want a transpiled code.

let ab = (a, b) => a;
let ba = (b, a) => a;

if (Math.random() > 0.5) {
  ab(true, false);
} else {
  ba(false, true);
}

Or something more sofisticated and probably more realistic like:

let ab = (a, b) => a;
let ba = (b, a) => a;

let fn = Math.random() > 0.5 ? {_f:ab, _h:{a:1,b:2}} : {_f:ba, _h:{b:1,a:2}};

_resolve(fn, {a : true, b : false});

I strongly suspect that for this to be adopted you have to be able to transpile the function definition site and the call site in place, which is to say that the definition and the call site are in different files it must be possible to transpile each file independently and get a working result.

The good news is that that doesn't rule out the whole solution space. WeakMap is an especially potent primitive for this kind of design problem.

const argNames = new WeakMap();

let ab = (a, b) => a;
argNames.set(ab, { a: 0, b: 1 });
let ba = (b, a) => a;
argNames.set(ba, { a: 1, b: 0 });

let fn = Math.random() > 0.5 ? ab : ba;

_resolve(fn, { a: true, b: false });

It's basically the same thing but now you don't have to touch the Math.random line at all which is very good in terms of this being practical.