While converting a JavaScript library protocol into TypeScript, I stumbled upon use of leading optional parameters, as opposed to regular / trailing ones.
A method in JavaScript:
db.task(function (context) {
// executing task
});
has an optional name for the task that can be injected in front:
db.task('myTaskName', function (context) {
// executing task
});
This is done to make the code more readable, having the task name up in front, as opposed to somewhere in the end, which would look wrong / unintuitive.
How does one code around such parameters in TypeScript?
I know I can declare both parameters as optional, but this wouldn't be the case, because the callback function is required as either the first or the second parameter. And if it makes this any simpler, we could say - the last parameter must be a callback function.
While converting a JavaScript library protocol into TypeScript, I stumbled upon use of leading optional parameters, as opposed to regular / trailing ones.
A method in JavaScript:
db.task(function (context) {
// executing task
});
has an optional name for the task that can be injected in front:
db.task('myTaskName', function (context) {
// executing task
});
This is done to make the code more readable, having the task name up in front, as opposed to somewhere in the end, which would look wrong / unintuitive.
How does one code around such parameters in TypeScript?
I know I can declare both parameters as optional, but this wouldn't be the case, because the callback function is required as either the first or the second parameter. And if it makes this any simpler, we could say - the last parameter must be a callback function.
Share Improve this question edited Jan 22, 2018 at 15:44 vitaly-t asked Mar 28, 2016 at 14:45 vitaly-tvitaly-t 25.9k17 gold badges127 silver badges150 bronze badges 4-
1
@fireydude In JavaScript all function parameters are optional and untyped, and you'd essentially have
(nameOrCallback, callback)
, then check ifcallback === undefined
to figure out hownameOrCallback
should be treated. I wish no one wrote code like that. :) – Aaron Beall Commented Mar 28, 2016 at 15:35 - @Aaron writing code like this is indeed ugly, but from the usability point of view it makes sense, as explained in my example. – vitaly-t Commented Mar 28, 2016 at 16:02
- @vitaly-t The usage is readable, but the cost is that the API has multiple meanings for a single argument depending on order. I personally don't think that's a good trade-off, given there are plenty of other ways to make it readable. Just my opinion I suppose. – Aaron Beall Commented Mar 28, 2016 at 16:07
- The accepted answer has a few issues. Please see my answer. – basarat Commented Mar 29, 2016 at 3:39
2 Answers
Reset to default 12The accepted answer uses a few hacky things:
- Type Assertion : Do not use this unless you have to. It is dangerous : https://basarat.gitbooks.io/typescript/content/docs/types/type-assertion.html Instead one should use a type guard : https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html
- Leaves the method open for erroneous calls (e.g.
myDefinedMethod('test')
i.e. no callback is provided). Instead one should use function overloading : https://basarat.gitbooks.io/typescript/content/docs/types/functions.html
The right way to do this
Here is an example:
type Cb = (context: any) => any;
function task(cb: Cb);
function task(name: string, cb: Cb);
function task(nameOrCb: string | Cb, cb?: Cb) {
if (typeof nameOrCb === 'string') {
const name = nameOrCb; // You can see that `name` has the inferred type `string`
// do something
}
else {
const cb = nameOrCb; // You can see that `cb` has inferred type `Cb`
// do something
}
}
// Tests
task((a) => null); // Ok
task('test', (a) => null) // Ok
// Type Safety
task((a, b) => null); // Error: function does not match type cb
task('test'); // Error: `cb` must be provided for this overload
If you mean when you are defining parameters in your function you would mark them with the ?
symbol for optional and use the |
to show the various types that could be injected. In the function itself you have to see what was passed in to which function. RequireJS
does this in their define functions and they have a .d.ts
(definitely typed) file that shows this.
Example with a method/function
// definition
function myDefinedMethod(callback: (someVar:any)=>any):void;
function myDefinedMethod(name: string, callBack: (someVar: any) => any): void;
function myDefinedMethod(nameOrCallback: string | ((someVar:any)=>any), callBack ?: (someVar: any) => any): void {
var name = "";
if (typeof nameOrCallback === 'string') {
name = nameOrCallback;
}
else {
callBack = nameOrCallback;
}
// both name and callback are now defined although name can be empty
// do something
console.log(name);
}
- This approach uses method overloading to ensure type safety, this will prevent a caller from calling the method with only a string at transpile time (as typescript is not piled).
- In the 3rd function definition the parameter
nameOrCallback
could be either the function or the name of the parameter. If it is a string then thecallback
cannot be undefined.
Edit
Thank you @basarat, I have updated my answer based on your feedback. This is indeed a better structure as you are ensuring the caller cannot execute your method without supplying the expected mandatory parameters like the callback. I did use Function
before but only as a placeholder for whatever function definition that the OP would want to use and was not intended as a final type parameter. To clarify this I have updated the code with an inline callback definition like yours.
Again, thank you for your input. This does indeed make the code better by ensuring type safety and ensuring that a caller can only call the method as intended.