Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Named parameters/arguments in function call #467

Closed
ivogabe opened this issue Aug 16, 2014 · 57 comments
Closed

Named parameters/arguments in function call #467

ivogabe opened this issue Aug 16, 2014 · 57 comments
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript

Comments

@ivogabe
Copy link
Contributor

ivogabe commented Aug 16, 2014

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);
@basarat
Copy link
Contributor

basarat commented Aug 16, 2014

👍 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)

@ivogabe
Copy link
Contributor Author

ivogabe commented Aug 16, 2014

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.

@basarat
Copy link
Contributor

basarat commented Aug 16, 2014

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

With this I don't see any issues :)

@saschanaz
Copy link
Contributor

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

@ivogabe
Copy link
Contributor Author

ivogabe commented Aug 17, 2014

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."

@saschanaz
Copy link
Contributor

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

@RyanCavanaugh
Copy link
Member

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

@ivogabe
Copy link
Contributor Author

ivogabe commented Aug 18, 2014

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)

@basarat
Copy link
Contributor

basarat commented Aug 19, 2014

@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

@ivogabe
Copy link
Contributor Author

ivogabe commented Aug 19, 2014

@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

@ghost
Copy link

ghost commented Aug 19, 2014

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

@ivogabe
Copy link
Contributor Author

ivogabe commented Aug 19, 2014

@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.

@ghost
Copy link

ghost commented Aug 19, 2014

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.

@basarat
Copy link
Contributor

basarat commented Aug 20, 2014

@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.

@mindplay-dk
Copy link

@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.

@kitsonk
Copy link
Contributor

kitsonk commented Dec 1, 2015

(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.

@RyanCavanaugh RyanCavanaugh added Out of Scope This idea sits outside of the TypeScript language design constraints and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Dec 1, 2015
@RyanCavanaugh
Copy link
Member

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

@ghost
Copy link

ghost commented Dec 4, 2015

@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?

@ghost
Copy link

ghost commented Dec 4, 2015

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.

@mhegazy
Copy link
Contributor

mhegazy commented Dec 4, 2015

@ghost
Copy link

ghost commented Dec 4, 2015

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

@DanielRosenwasser DanielRosenwasser changed the title Named arguments in function call Named parameters/arguments in function call Jan 6, 2016
@ghost
Copy link

ghost commented Feb 26, 2016

@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.

@RoyTinker
Copy link

RoyTinker commented Dec 30, 2017

@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.

@mindplay-dk
Copy link

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.

@HamedFathi
Copy link

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

@mindplay-dk
Copy link

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

@sheerun
Copy link

sheerun commented Mar 9, 2018

Related: microsoft/vscode#16221

@doivosevic
Copy link

@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.

@RyanCavanaugh
Copy link
Member

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

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

@wongjiahau
Copy link

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 

@qoh
Copy link

qoh commented Sep 30, 2018

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
}

@zen0wu
Copy link

zen0wu commented Oct 22, 2019

@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);
  }
}

@mindplay-dk
Copy link

mindplay-dk commented Oct 22, 2019

@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?

@doivosevic
Copy link

@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

@mindplay-dk
Copy link

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?

@kataik
Copy link

kataik commented Feb 14, 2020

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.

@zlamma
Copy link

zlamma commented Feb 15, 2020

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.

@Rocamonde
Copy link

Rocamonde commented Jul 10, 2020

@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.

@Rocamonde
Copy link

Rocamonde commented Jul 10, 2020

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.

@MartinJohns MartinJohns mentioned this issue Aug 1, 2020
5 tasks
@d8corp
Copy link

d8corp commented Aug 3, 2020

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}}) {}

@zlamma
Copy link

zlamma commented Sep 25, 2020

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests