-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Comments
👍 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 // 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) |
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 |
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? |
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(z, changeZ()); Which is the same as 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 ( |
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
will confuse with the other sample (by reading it looks like foo(d: 8, 5); // e will be 5 |
@basarat It's allowed to write 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 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:
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)
Non-goal:
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: 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
|
@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.
is basically syntactic sugar for
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:
It can be called using:
A default could then be added:
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 For me, this is immensely useful for readability and refactorability especially with large-ish method signatures like in some factory functions. |
@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. |
@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. |
Related: microsoft/vscode#16221 |
@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 |
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
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 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:
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 |
@DominikDitoIvosevic No, I don't think it works without some sort of operator?
The parser would see this as destructuring of the arguments, I think? It can't know if |
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 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.:
As such, the In any case, regardless of how the 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
This reminds me of that argument for C++ named arguments, according to proposal N4172:
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. |
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). |
C# allows adding names of arguments in a function call:
See http://msdn.microsoft.com/en-us/library/dd264739.aspx
In TypeScript's source this is also done, but with comments:
Can we have named arguments in TypeScript like this:
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:
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:
Generates
The text was updated successfully, but these errors were encountered: