Typescript: Named parameters/arguments in function call

Created on 16 Aug 2014  ·  57Comments  ·  Source: microsoft/TypeScript

C# allows adding names of arguments in a function call:

CalculateBMI(weight: 123, height: 64);

See http://msdn.microsoft.com/en-us/library/dd264739.aspx

In TypeScript's source this is also done, but with comments:

emitLinesStartingAt(nodes, /*startIndex*/ 0);

Can we have named arguments in TypeScript like this:

emitLinesStartingAt(nodes, startIndex: 0);

This can add some compile time checking, for instance when you type count: 0 you will get an error.

I think we should add a restriction that the order of the arguments cannot be changed. Example:

function foo(first: number, second: number) {}
var z = 3;
foo(second: z++, first: z);

An error should be thrown on the last line, because changing the order of the arguments (to foo(z, z++)) in the generated javascript would cause unexpected behavior.

Also this can be useful for functions with lots of optional arguments:

function foo(a?, b?, c?, d?, e?) {}
foo(e: 4);
foo(d: 8, 5); // e will be 5

Generates

function foo(a, b, c, d, e) {}
foo(void 0, void 0, void 0, void 0, 4);
foo(void 0, void 0, void 0, 8, 5);
Out of Scope Suggestion

Most helpful comment

What if I wish to do something like this?

constructor({
  public id: string,
  public name: string,
  public email: string,
}) { }

What TypeScript requires me to do to achieve that is not very nice:

public id: string
public name: string
public email: string

constructor({ id, name, email }: {
  id: string,
  name: string,
  email: string,
}) {
  this.id = id
  this.name = name
  this.email = email
}

All 57 comments

:+1: but don't think it will work as proposed. Changing the call site based on type information is a bad idea (agree with the lessons here #9)

Some form of codegen at the function would be able to do this. Here's what I use personally (actually I just use || generally but wouldn't work for bool, or when 0 is a valid value):

// Only once: 
function def(value, def) {
    return (typeof value === "undefined") ? def : value;
}

/// generally
interface Body {
    weight?: number;
    height?: number;
}
function calculateBMI(body: Body= {}) {
    body.weight = def(body.weight, 0);
    body.height = def(body.height, 0);
}

It is more code but definitely better than _inline_ comments (which are pretty fragile)

Changing the call site based on type information is a bad idea

The difference is that in this suggestion you will get an error message, and in the other issue you won't get an error. Using named arguments on a function typed any should be illegal in my opinion, since the compiler doesn't know the index of the argument with that name.

Using named arguments on a function typed any should be illegal in my opinion

With this I don't see any issues :)

Hmm... Why should we add the order restriction? Isn't it OK to force the compiler to correct the order?

Why should we add the order restriction?

Example:

function foo(first: number, second: number) {}
var z = 3;
function changeZ() {
    z = 8;
    return 1;
}
foo(second: changeZ(), first: z);

You would expect that the last line is the same as foo(second: 1, first: 8); and foo(8, 1). If the compiler would correct the order, the generated javascript would be:

foo(z, changeZ());

Which is the same as foo(3, 1);, not foo(8, 1). The compiler could correct that:

var __a;
foo((__a = [changeZ(), z], __a[1]), __a[0]);

But that's some ugly code in my opinion, and one of TypeScript's design goals is to "emit clean, idiomatic, recognizable JavaScript code."

Oh, I agree with that. Thank you for clarification :)

What's the behavior with rest args (f(x: string, ...y: any[])) ?

It think it should be allowed to call that function this way:

f(x: "foo", y: 3, 4, 5)

And not

f(x: "foo", y: 3, y: 4, y: 5)

@ivogabe The following

f(x: "foo", y: 3, 4, 5)

will confuse with the other sample (by reading it looks like d is 8,5):

foo(d: 8, 5); // e will be 5

@basarat It's allowed to write foo(d: 8, e: 5), but I don't want to force to add names to all arguments. An alternative for rest arguments would be to add three dots (f(x: "foo", ...y: 3, 4, 5)). I first didn't think about this because the name of the argument makes already clear that it can except multiple arguments:

list.insert(index: 3, values: 7, 8, 9); // values (plural) makes clear that there are multiple values coming.
// alternative:
list.insert(index: 3, ...values: 7, 8, 9);

I also thought about overloads, when a function has overloads, the selected overload should be used for the names in my opinion.

function overloaded(num: number): number;
function overloaded(str: string): string;
function overloaded(a: any): any { return a; }

overloaded(num: 5); // Ok
overloaded(str: 5); // Error
overloaded(a: 5); // Error

This would be very foreign from javascript requiring boilerplate overload-resolution and routing code in the target output.

@Gitgiddy Can you give an example of what you mean? This suggestion doesn't change a lot of the overload-resolution algorithm in the compiler. The only case where this is changed is:

function f(a?: string, b?: number);
function f(b: number);
function f(x?: any, y?: number) { }

f(b: 3); // Which signature is called?

In cases when multiple overloads can be used, the compiler chooses the signature that is sees first, so I think it should do the same here. There is a difference in the generated javascript:

f(void 0, 3); // first signature
f(3); // second signature

I think that there is not a lot of code that has these kind of overloads.

I didn't read carefully enough, I guess. Maybe I'm still not. But I don't see the advantage of specifying the parameter names, especially if, as you do a good job of explaining but I missed the first time, the parameter order cannot change at compile time. This is just so you can skip some non-trailing params? Intellisense does a good job of telling you which arguments map to which parameters. Overloads still all point to just one javascript function implementation.

@Gitgiddy consider the case where to solve some problem you start with

function start(isFoo?:boolean, isBar?:boolean: isBas?:boolean){}

But as you understand the problem you realize you don't need isBar anymore. It is not possible to do this safely without named parameters (which is why I use object literals) + its not clear at the call site e.g.:

start(true,false); // What is true, what is false?. Its easier if its explicit.

@basarat in Typescript, interfaces are "free" - there is no run-time footprint at all, which is why I prefer this approach:

interface StartParams {
    isFoo?: boolean;
    isBar?: boolean;
    isBaz?: boolean;
}

function start(p: StartParams) {
    // ...
}

start({ isFoo:true, isBaz:false });

It's almost like having named arguments, though not quite, since you still have to unpack the arguments inside the function. I often find this approach to be safer and easier to refactor over time though. YMMV.

(Said originally this on #5857)
Personal opinion... it is anti-TypeScript design goals:

  1. Avoid adding expression-level syntax.

Non-goal:

  1. Exactly mimic the design of existing languages. Instead, use the behavior of JavaScript and the intentions of program authors as a guide for what makes the most sense in the language.

Object destructuring in ES6 with the combination of the terse object literal declarations in ES6 (both downward emittable) are more than sufficient to cover this type of functionality. TypeScript isn't trying to be a new language, it is trying to be a superset of JavaScript. This sounds a lot like a new language.

Agreed. Parameter destructuring covers this scenario very well and it's not clear we could ever emit this correctly in all cases.

@RyanCavanaugh Parameter destructuring mostly does cover this but the syntax with TS can be pretty bad if you want to support 0 arguments. Let's say you want a function that logs two booleans, taking 0, 1, or 2 parameters:

function log({a = true, b = true} = { a: true, b: true }) {
    console.log(a, b);
}

log();
log({a: false});
log({b: false});
log({a: false, b: false});

In ES6 without TS you could just do this:
function log({a = true, b = true} = {}) { ... }

But that doesn't work for TS because the types won't be equivalent and you get an error, so you have to repeat the structure twice.

Is there a better way of doing this in TS?

Hmm, I'm actually not seeing an error for this on TS Playground. Perhaps this didn't work in TS 1.5 but has been fixed in 1.6 or 1.7? This is the error I'm getting in 1.5 when I use just {} for the default argument:

error TS2459: Build: Type '{}' has no property 'a' and no string index signature.
error TS2459: Build: Type '{}' has no property 'b' and no string index signature.

@alanouri, that was an TS 1.7 enhancement, you can read more about it in https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#improved-checking-for-destructuring-object-literal

Oh, yes that's exactly this issue. Perfect, thanks for the enhancement, I think this works perfectly now.

@alanouri gives a great example of a default case above, but I just wanted to give a non-default example. I know this issue is closed, but it's the first hit on google and I wanted to explicitly show another example using destructuring for others like me who don't understand what this means.

function log({a = true, b = true} = { }) {
    console.log(a, b);
}

is basically syntactic sugar for

function log({a = true, b = true}: {a?: boolean, b?: boolean} = { }) {
    console.log(a, b);
}

But this does several things and this may not be obvious to others (like me) who are relatively new to TS. It gives defaults to individual values, as well as a default to the entire value.

Here is another example without some of the defaults:

                // params    // shape
function logNum({num, a, b}: {num: number, a?: boolean, b?: boolean}) {

    console.log(num, a, b); // << uses destructuring
}

It can be called using:

logNum({num: 123});
logNum({num: 123, a: false});

A default could then be added:

                                                                          // default
function logNum2({num, a, b}: {num: number, a?: boolean, b?: boolean} = { num: 123 }) {
    console.log(num, a, b);
}
logNum2();

There are many more possibilities, but the important part is that you are defining a single parameter object, the shape of that object, and sometimes defining optional/default values. Destructuring is then pulling out the individual args inside the implementation, as in the line console.log(num, a, b);.

For me, this is immensely useful for readability and refactorability especially with large-ish method signatures like in some factory functions.

I wanted named arguemnts for a while, and I changed my mind.

Javascript already has idiomatic named arguments, in the form of parameter objects.

In Typescript, these are even more useful, because you can declare an interface, defining all the possible parameter types.

Thanks to declaration merging, decorators/plugins/etc can even decorate such an interface with new possible parameters.

It's quite flexible - more so than named arguments would be. But more importantly, it's idiomatic - there is myriad JS code in the wild using the parameter object pattern for just about anything.

As for defaults, the typical JS approach is not to do it at the moment when you _receive_ the parameters, but rather to do in when you _consume_ one of the parameters - so rather than establishing defaults up front, typically we specify the default when we look up the parameter, e.g. something like $.post(options.url || DEFAULT_URL).

In other words, JS developers are used to seeing the default specified at the point where you consume an optional parameter, not where you received it. This also has the advantage of being able to very the default in each case, e.g. options.url || DEFAULT_URL in one case, and options.url || ANOTHER_URL in another case.

I think this feature is unnecessary, and in recent years, in other languages, I find myself leaning towards the parameter object pattern, for any large-ish method signature, such as factory functions, as it tends to refactor more easily without BC breaks.

Having optional arguments in a signature, tends to indicate that those arguments aren't directly or closely related, and often then a parameter object is preferable to me.

Anyways, just putting that out there for anyone to ponder, if you're still obsessing about the lack of named arguments. I rarely miss them anymore :-)

Since we're talking about parameters destructuring,
is there a way to automatically assign destructured constructor parameters to the class instance,
like it is possible adding the private/public/protected keyword in front of the parameter name?

is there a way to automatically assign destructured constructor parameters to the class instance,
like it is possible adding the private/public/protected keyword in front of the parameter name

Nope :rose:

In my experience, the primary utility of named parameters is to increase readability at call sites by providing a sort of inline documentation -- a sort of "here's what this argument is, so you (the developer) don't have to look it up".

Yes, JS provides idiomatic named arguments already, in the form of parameter objects. However, a very large number of libraries have functions/methods with multiple formal parameters.

Since TypeScript is meant as ES + type annotations/checking, I don't think named parameters as a TS feature would make any sense as an output transformer. However, I think it would be very useful as an optional, syntax-only, semantic no-op --- a sort of cleaner alternative to inline comments that could improve readability.

A setTimeout example:

setTimeout(() => {
  console.log("Print this after a while");
}, waitTime: 1000);

Here the waitTime annotation is nice and reduces cognitive load by a small amount (in my opinion). It feels like a kind little "here I gave it a name" to other developers; whereas an inline comment like /* waitTime */ would look ugly and almost insulting , hence we only ever see it when working with arcane libraries or long function signatures.

Somewhat orthogonally, I imagine named parameters might also be useful as a compile-time check for verifying argument names against other TypeScript (or JS+types) code, with errors thrown when the names don't match. But I haven't thought enough about that case to say much more...

@RoyTinker that seems like something which should be proposed for standardization into ECMAScript. Adding it on the TypeScript side introduces a massive likelihood for collisions with the subset.

@aluanhaddad Good point, thanks. Although I imagine they'd have a similar concern about collisions with potential future features 🤷‍♂️

Named parameters are handy for adding new arguments to a legacy API. In this scenario, destructuring isn't an option because it would break existing function calls.

Without named parameters:

login(Model.Email, Model.Password, undefined, undefined, undefined, Model.invitationID)

Notice the 3 nasty undefineds for unused optional arguments. Unfortunately, I can't change the order of these arguments without breaking other parts of the application.

With named parameters:

login(email: Model.Email, password: Model.Password, invitationID: Model.invitationID)

@RyanCavanaugh

can we at least allow named arguments in the same order as parameters go?

point is the names help a lot when it comes to reading the code

since the order of arguments and parameters always the same there is no problem with matching

with that said the emit can just drop these names (for the sake of transpile to js scenario)

if the type information is available the argument names are forced to match the parameter names

please consider reopening this issue

It's pretty risky to add expression syntax in that position; I don't think what is essentially documentation meets the bar for that.

In TS we just use fn(/* paramName */ arg) when the meaning is not obvious from context. We had a lint rule to enforce the matching as well (though this turns out to be expensive)

can we at least allow named arguments in the same order as parameters go?

Just a thought, but - given the example above...

setTimeout(
    () => console.log("Print this after a while"),
    1000 // waitTime
);

And the other one...

login(
    Model.Email,
    Model.Password,
    undefined, // no first name
    undefined, // no last name
    undefined, // dob (not available on this form)
    Model.invitationID
)

Looks better to me: you didn't need to repeat the words "email" and "password", since those words figure in the arguments you're passing, and it makes room to explain why you're omitting certain values.

The upshot of this is that you're not at risk of the actual argument names changing... I mean, normally, changing an argument name is not a breaking change - call sites aren't affected.

Also, arguments in JS functions obtain names for positional arguments only at call-time inside the function scope - if you create dependency on argument names, arguably (hah!) that might actually be considered a change to the semantics of the language?

@mindplay-dk The callee parameters are not always so close on levels of abstraction as the arguments of the call. Also, the way to call can be ambiguous. If that's the case, code expressions as arguments don't really help in understanding the call.

Consider the contrived example of:
transferCredits(from: AppInfo.AppOwnerCompanyAccount, to: Model.LoggedInUserAccount)
vs
transferCredits(AppInfo.AppOwnerCompanyAccount, Model.LoggedInUserAccount).

In all cases, this was about introducing an optional feature. It can be only applied where it makes sense. Also if anyone really prefers any other way, like comments above, they could still rather use that.

@zlamma the main problem with this idea as a compile-time feature, is the semantic mismatch: if somebody renames a positional argument, that's not traditionally a breaking change at any call-site - if we allow named arguments at call-sites for positional arguments, renaming a positional argument is now a breaking change; but you can't reasonably expect neither JS or TS library vendors to version their packages as major releases every time they rename an argument, because, according to how JS works, positional arguments are assigned names only within the local scope of the function, at call-time.

So the whole discussion is kind of a non-starter: it's simply not how JS works. No matter how much I like (and would like to have) this feature, even if you took this to the TC39, I think they would reject it more or less outright, because it would be a breaking change to the language.

I think you're going to have to resign yourself to using parameter objects in your own code - and, when you need to use third-party library code, and positional arguments cause serious readability problems for you, you will have to either PR and help improve those libraries, or, proxy third-party functions in custom wrapper-functions that do use parameter objects.

I simply don't see any other ways to approach this - even the vestigial naming feature (as you or someone else proposed) where you can name parameters only when also supplied in positional argument order, doesn't make sense, since, again, changing the names of positional arguments isn't supposed to be a breaking change.

@mindplay-dk Great points - it seems clear that named positional parameters as a check and/or an output transformer are inadvisable in both TS and JS.

However, please see my suggestion above - that isn't supposed to be capable of causing breaking changes. It would only be a nicer alternative to using inline /* parameterName */ comments.

@mindplay-dk What if named parameters were added as a semantic no-op (call site documentation) as I mentioned above, but also an "experimental" TS compiler feature (opt-in-only) that would throw errors for mismatched TS-to-TS parameter names? In my mind, this could cause breaking changes (when library APIs or type annotations change, etc.), but only when someone wants that behavior.

@RoyTinker fwiw, when (only) implemented as a semantic no-op this starts to look a lot more like an IDE feature, than a language construct. https://www.youtube.com/watch?v=ZfYOddEmaRw

@mindplay-dk - whether this is a breaking change is rather subtle. It is certainly not a breaking change for any existent code, neither libraries nor their callers - the syntax is not used, since it is unavailable. The behavior is breaking just to 'expectations about new code', which is much much milder.

And here, take note that the considerations about whether an error raised by the program in a new situation is a 'breakage' or a 'feature' are very different when the program in question is a _type-safety static code analysis tool_, whose goal is to find errors, contradictions and ambiguities that inevitably occur as a project integrates changes made independently to multiple components.

Machines can only care about position of the arguments because they solve their need with what requires the least computation, but human intentions when writing a program are certainly to provide an argument with specific meaning, not an argument at a given position (except for functions that assign no identity to arguments, like the ones with variable arguments, but their calls would not use naming, so are irrelevant here). Given that, it's pretty likely that when the name changes, it no longer means the same, so it's actually very reasonable to notify a human to have him establish which is the case. I, for one, would appreciate getting notified and not make much fuss even if it turned out that I just need to do the trivial chore of changing the name. That is one of the reasons I specified the name in the first place (the other being just readability). Moreover, I will still want that behavior whenever I decide to use the feature, even in a call to pure-JS library functions which don't flag parameter name changes as breaking in their SemVer. I am convinced that it's the same for most if not all programmers. TypeScript would still do a service to the programmer who specified a name in the call by failing, even if the user didn't know that the function was pure-JavaScript. It's raising a valid request to dispel a new ambiguity because the user _opted in_ for this very check.

I really think that the feature is too useful in all natural situations, for some 'contrived scenario where this error would be undesired' to decide its fate. And I really think now that this scenario is a purely contrived one, because for this error to cause bad consequences (of causing real damage) rather than good consequences (of notifying a human in face of a new ambiguity and getting him to resolve it) this compilation error would need to not happen during local development (and these days this is where it will usually only happen - after all, lock files are generated by default by package managers and programmers are instructed commit them) or not even in the build pipeline, but it would need to happen in a compilation that was run against the 'somehow updated' JavaScript package, and that compilation would need to happen for the first time on production. Surely TypeScript should be run in a place that developers monitor, so the build pipeline is really the last place to do it.

And I would perhaps understand the conservatism if the chore of fixing such renames was expected to be forced on the programmers very often (like 'on on every other library update') and requiring plenty of time (it would need to occur very very often, for a trivial chore like this to require plenty of time). But here, the reality will be quite the opposite - reacting to the error is going to cost minimal time and it's even going to be really really rare. I say that because I can't remember when all odds like this came together for me in a statically typed language: when a library changed an argument name without changing its meaning while I had the name specified (perhaps that's because a name needing change without changing meaning very often coincides with the old name not being very helpful, which would make me avoid specifying it).

Lastly, all of the points, which you made about this 'breakingness' of the feature, used to apply to any language that introduced the feature mid-life. Notably, C# did this in 4.0, Ruby in 2.0, Scala in 2.8. And the 'semantic mismatch' applied there too, even if just temporarily: programs written in the new language versions still work with libraries written in the old one, and these libraries can still produce new packages without changing the language version. The introduction of this feature did not result in any revolution or a serious issue for the users of these languages.

@zlamma Good points. It's also good to know details on other languages. There's one important difference b/t TypeScript and those languages: TypeScript is transpiled to ES, and one of its stated goals (#​8) is to "avoid adding expression-level syntax" -- of course, excluding type annotations. Goal #​9 is to "use a consistent, fully erasable, structural type system".

Since this feature would contribute to compile-time checks, would be fully erasable, and would not affect compiler output, I think it's arguable that it falls under goal #​9 instead of the syntactic exclusion under #​8.

What if named parameters were added as a semantic no-op (call site documentation) as I mentioned above, but also an "experimental" TS compiler feature (opt-in-only) that would throw errors for mismatched TS-to-TS parameter names?

@RoyTinker but it's the same problem: it's a breaking change to TS, in the sense that anyone versioning their packages using SEMVER will now have to version a parameter name change as a breaking change: package authors don't do that at this time, and even if they did, packages written in TS are going to get compiled and release as JS, and the versioning (normally) follows the TS (as the source and output JS are typically one package/repo) - and now you have non-breaking changes in the JS versioned as breaking, which, again, points at a (however minor) change to language semantics.

Maybe it's strange and stupid, but why can not you do that in this way?

for example

private test(a:number, b:number, c?:number, d?:number, e?:number) { 
}

use it like

private test(1, 2, e::5) // I am not sure about :: !
{
}

so, Typescript compiler can generate the js like

private test(1, 2, undefined, undefined, e) { 
}

Anyway, I'm not professional in this area

@HamedFathi the syntax is not the issue at all - you should read through some of my recent comments.

@RyanCavanaugh would it be possible to reconsider only a case where parameter order is as original? Whenever I have multiple parameters of same type (like string) it's a mess trying to figure out which of them is for which value.

No, because the concern of syntactic conflict with a future version of ECMAScript still stands.

We use the /*name*/ syntax when this occurs

Just for those who wanted this feature, you can actually emulate it using object-destructuring since ES6. For example,

// Definition
function send({message, to}) {
    if(to === "console") { console.log(message) }
    else if(to === "window") { window.alert(message) }
    else { console.log("Unknown channel : " + to); }
}

// Example of usage
send({message: "Hello world", to: "console"})
send({message: "Hello world", to: "window"})

// Tested in Google Chrome 

What if I wish to do something like this?

constructor({
  public id: string,
  public name: string,
  public email: string,
}) { }

What TypeScript requires me to do to achieve that is not very nice:

public id: string
public name: string
public email: string

constructor({ id, name, email }: {
  id: string,
  name: string,
  email: string,
}) {
  this.id = id
  this.name = name
  this.email = email
}

@qoh This is what I do. Not super pretty, with two issues

  1. Shape doesn't work very well with methods
  2. When strict is enabled, all the properties will have to have !
type Shape<T> = { [P in keyof T]: T[P] }

class X {
  public id!: string
  public name!: string
  public email!: string

  constructor(obj: Shape<X>) {
    Object.assign(this, obj);
  }
}

@wongjiahau's example with ES6 object destructuring is probably the closest we can realistically get to named arguments in TS without departing from JS semantics?

That pattern is already idiomatic to JS, and engines likely already optimize for it.

Perhaps we should start asking how to better enable that pattern in TS instead?

So how do we improve on this:

function send({message = "Hello", to}: {message: string; to: string}) {
  // ...
}

Repeating every argument (property) name is a bit clunky, but it's also the only real problem - call sites are perfectly readable with an object literal, and such calls are completely idiomatic to JS since the dawn of time, so no real problem there IMO.

Here's an idea for a simple sugar to abbreviate the example above:

function send(${ message = "Hello", to: string }) {
  // ...
}

This syntax would work in argument lists only - it just declares a single argument that desugars to an ES6 object spread, with an anonymous type assembled from the argument-names/types, which can be either declared with : or inferred from a default const initialization with =.

The syntax would be more or less exactly that of property-declarations in a class.

I believe this approach has a few advantages over actual named arguments:

  1. You can have more than one set of named arguments:
    function foo(${ a: string, b: string }, ${ c: string, d: string })

  2. You can dynamically define named arguments at call sites:
    bar({ ...defaults, ...overrides, some: value })

  3. Named arguments are properly grouped - so mixing positional and named arguments is clear:
    function baz(a: number, ${ b: string, c: string }, d = "default")

I think there's a bunch of advantages to building on established semantics and well-known patterns, rather than inventing an entirely new language feature.

Thoughts?

@mindplay-dk I actually like this proposal quite a lot. Do we need the $ tho? I guess the parser could know what we want even without it

Do we need the $ tho? I guess the parser could know what we want even without it

@DominikDitoIvosevic No, I don't think it works without some sort of operator?

function foo({ a: string }) {}

The parser would see this as destructuring of the arguments, I think?

It can't know if string is type or a variable-name, so it's ambiguous with destructuring the property a into a variable named string, isn't it?

Hi everyone, I'm a bit confused. I can use object destruct to force the input object approach on my function's customers only if I have the luxury to change the function signature. For functions requiring backward compatibility or functions I consume but not own, this is not a solution.

I agree with @kataik. Overall, the suggestions to refactor the problem away using an {...} object argument are not satisfactory as a 'language-wide' solution to the problem, no matter the syntactic sugar like the recent proposal from this comment.

Backward compatibility mentioned by @kataik above is one reason, the need for which occurs all too often. As an example, even Google's JS style guide recognizes that it can be _'infeasible'_ to refactor functions' multiple parameters away.

But there are more reasons, because refactoring to a single 'object parameter' brings differences that are not merely syntactic - they also have consequences for the features of the program, e.g.:

  • choosing multiple function arguments produces a more performant function (no new heap object allocation/garbage collection)
  • it also enables partial application using bind (likewise, this partial application will be more performant than any object-based alternative, because the runtime can optimize it better, even if for some browsers you had to wait for the speed-up).

As such, the {...} convention will never reach full adoption within the language, even despite it being generally suggested as the JavaScript's idiomatic way of passing named arguments. A convention that is opinionated _against_ the language's own features of unique value and better performance outcome cannot become a 'best practice' of that language.

In any case, regardless of how the {...} convention fares in adoption, I still stand by the conviction that adding the named arguments to calls on multi-parameter functions is a safe-to-introduce and a positive feature, and it is such even in the event of _'plain JS libraries changing their param name but not changing their SemVer'_.

Please see this previous comment of mine which I now updated, in an effort to demonstrate that making the compilation raise errors even in the case of plain JS libraries is an added value to the users of TypeScript in pretty much any real-life development setups.

@mindplay-dk
You mention that

changing the names of positional arguments isn't supposed to be a breaking change.

This reminds me of that argument for C++ named arguments, according to proposal N4172:

Objection #3: Named arguments make parameter names part of a function's interface, so that changing the parameter names can affect call sites
We were initially concerned about this as well. To get an idea about the magnitude of this problem, we surveyed some large open-source libraries to see how often parameter names change in the declarations of the public API functions (i.e., the names that users would use when making calls to these functions).

For each library, we chose a recent release and a release from several years ago, examined the diff between the two versions, and recorded the number of parameter name changes. Only name changes that preserved the types of the parameters were considered, as a change to the type of a parameter typically breaks a function's interface already. The table below summarizes our findings. For more details, please see [1].

Given the low number of parameter name changes relative to the sizes of these libraries over periods of several years, we believe that code breakage due to parameter name changes would not be a significant problem in practice.

I am sure that little popular libraries tend to frequently rename parameters within their development flow from stable version to another -- as this generally indicates poor coding practices. Clearly, if the function signature is replaced because of a semantic change, then it is only natural that this change breaks compatibility to some extent.

If code clarity and quality is in mind, well-planned projects should almost never refactor their parameters, and the cases where this would be so can also apply to any other user-defined identifier within the language. So this argument seems to be a bit like "what if someone builds a bridge that relies on TypeScript function calls being forever positional?" We might as well remove alphanumeric identifiers from the language ;)

Regarding your point about parameter scope, as TypeScript transpiles to JavaScript, this is not a problem. This only affects function call sites, so named parameters could be syntactic sugar that requires the function signature type to be present at compile time, and replace the named call with a positional call.

On another note, I don't think that adding this feature would be almost like "a new language", while it is true that object destructuring could be enough to solve this. However, currently the way it is causes the duplication of object attributes, making it quite unreadable. It was one of the things that I disliked the most when I started learning TypeScript. When functions are annotated in-place, code formatters break the one-liners and split the signature into many many lines, making it super ugly. So this ends up making the type be extracted to an interface.

Solving this in a nice way would require adding another syntactic feature significantly different to current object type annotation principles.

The most obvious way that someone would expect to avoid duplication:

function foo({firstParameter, anotherParameter}: {firstParameter: string, anotherParameter: number}) {}

would imply, as suggested, removing the destructuring and just leaving the interface

function foo({firstParameter: string, anotherParameter: number}) {}

But in order for this to work, the interpreter would have to either (a) not consider the declaration an interface, or (b) inject the interface attributes to the scope, and this would not be very beautiful, as it would be inconsistent with current behaviour. So the most natural solution is dropping the braces and adding syntactic sugar to function calls, which is not incompatible with type annotation as this feature is not currently allowed in call site.

On another note

function test1 ({a:: string}) {}
function test2 ({a: b: string}) {}
function test3 ({a: b: number | string = 1}) {}
function test4 ({a: {b:: number | string = 1}}) {}

With no disrespect, all suggestions to enhance the object-passing argument syntax (e.g. comments 1, 2, 3 etc.), as much as they are an improvement which would be great for TypeScript, they are actually off-topic to this thread (a workaround at most).

As such, I'd like to suggest to raise them as separate suggestions/move the discussion to the existing suggestions (e.g. this one). This thread still requests named arguments for reasons that cannot be satisfied with passing objects (see this comment for justification).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Roam-Cooper picture Roam-Cooper  ·  3Comments

wmaurer picture wmaurer  ·  3Comments

bgrieder picture bgrieder  ·  3Comments

siddjain picture siddjain  ·  3Comments

manekinekko picture manekinekko  ·  3Comments