As a JavaScript developer, you surely have encountered some functions that require a lot of arguments to be called. Because the argument list is an Array-like object, all the values need to be set and so it may have given you a headache to understand the order and purpose of each argument.
Let see an approach to define developer-friendly function signatures with the named-arguments pattern and TypeScript.
Passing multiples arguments to a function can leads to several issues:
itemId
is fine but what are behind the null
and true
values ?null
.Those issues apply for both the developer calling the function and the one reviewing the resulting code.
The more arguments you pass to a function, the faster it will become a nightmare to call it.
TypeScript can help you by providing an IDE integration displaying the function input naming.
Type checking can also help you in some cases with the order.
But you still have to pass the full argument list even though you only want to use 2 of them. Plus the TypeScript « auto documentation » may not be available when performing a read-only review of some code, on GitHub for example
So let’s jump in a solution that resolve all those issues.
Named-arguments
pattern (with TypeScript)The trick is to "replace" the argument list by a single javascript object that will embed all the arguments as properties. This pattern is called `named-arguments`
This pattern has always been available but it has been made easy with es6 Object destructuring and TypeScript
Using a single object as a wrapper and using properties to pass arguments will resolve all the issues:
getItem({
id: itemId,
disabled: true,
})
The true
value makes perfect sense now!
Plus, we don’t need anymore to pass extra values for optional parameters. No more extra null
values !
getItem({
id: itemId,
createdAfter: date1,
createdBefore: date2,
disabled: true,
})
If we want to add some optional parameters, we just need to define the related properties (createdAfter
& createdAfter
)
So now, let’s have a look of such a function declaration
Using a single object as an argument will make it more obtrusive to guess the input and harder to retrieve the values?
function getItem(data) { // no idea of the data structure :(
// we have to retrieve the values one by one….
const id = data.id
const createdAfter = data.createdAfter || null // setting a default value
...
}
Not at all, thanks to ES6 object destructuring that can be used straight to function inputs
function getItem({ id, createdAfter = null, createdBefore = null, disabled = false }) {
// our arguments are listed back, with some default values !
console.log("my input id", id)
...
}
It looks like the classic Array-like argument list (positional arguments) with some extra brackets around {}
.
Notice that we are using default parameters to define optional arguments:
createdAfter = null, createdBefore = null, disabled = false
If the createdAfter
or createdBefore
property is missing (undefined
) the null
value will be set automatically.
disabled
argument if omitted will be set to false
If you are familiar with React you have notice that this mechanism is used for Component props, taking advantage that adding attributes to a HTML( JSX) element has the same behavior that adding properties to an object:
Although all the issues as been resolved using our object, the developer experience will be really smooth when coupled with TypeScript. Your function API will be auto-documented showing to the developer the available property names and their related types.
Sometimes, the name of a property is not enough to understand the type of the value that should be used. Should a date argument be a Date object ? A formatted string ? Or a timestamp ?
TypeScript will give you this information.
interface ApiInterface {
id: string, // oh ! So the id should be a string and not a number
createdAfter?: Date | null // date is a Date object ! +optional
createdBefore?: Date | null //optional
disabled?: boolean // optional
}
// just by reading the interface we get a clear understanding of the argument natures
function getItem({ id, createdAfter = null, createdBefore = null, disabled = false } : ApiInterface) {
...
}
The input interface gives a good understanding of the arguments used by the function.
At a glance we can see which arguments are optionals thanks to the ?
If you omit a mandatory argument, TypeScript will notify you.
So TypeScript will help both the developer calling the function and a reviewer reading the code from the function itself
Cherry on the cake, shortand property names can ease the process of calling such a function.
If you’re passing values using variable names matching the expected properties, you’ll just have to call the function wrapping the variables inside brackets
getItem({ id, disabled })
It is the same thing than
getItem({ id: id, disabled: disabled })
If one of your variable name doesn’t match an expected property name, you can of course mix shorthand property names with classic declarations.
getItem({ id: itemId, disabled })
The nice thing with the named-arguments
pattern is that the more arguments are required for a function, the more sense it will make to use it.
That’s not because this pattern has some advantages that you should use it everywhere:
If your function accepts a single argument, it doesn’t make sense to wrap it in an object.
You should just ensure that the naming give a good hint of the input nature. Using TypeScript won’t even make it an issue.
You could be tempted to use named-argument because of potential future evolution of your API but you shouldn’t forecast it by sacrifying simplicity.
If you need additional arguments later, let’s refactor it at this moment.
In my opinion, if your function only accepts 2 arguments, it is still excessive to use this pattern, especially if the second one is optional.
But it could be a good call to use named-arguments if the 2 properties have the same types.
For example building a range of values (min/max) or a range of dates (start/end)
The order may be important and TypeScript won’t help in this case.
isValidRange(startDate, endDate) // should startDate be the first argument ?
isValidRange({ start: startDate, end: endDate}) // no more ambiguity
Keep in mind that if you have a lot of properties in your argument object, this can be a good hint that you are doing too many things at once.
For example, if you return an object using the RORO (Receive Object / Return Object) pattern
, you could split your function into multiple sub tasks using composition.
As always, when your single input and output share the same type, it can be a good hint that you can enable composing, splitting the manipulation on your input data on multiple functions.