JavaScript smooth API with named-arguments and TypeScript

May 04, 2022 – Thibaud Esnouf – 9-minute read

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.

The original issues with functions and arguments

Passing multiples arguments to a function can leads to several issues:

Example with the infamous null in the middle
    getItem(itemId, null, null, true)
  • Unless some variables with good naming are used, you have no clue of what the values stand for.
    itemId is fine but what are behind the null and true values ?
  • The order is important. Miss it and your code is broken.
  • You can’t skip optional parameter if they are defined in the middle of the argument list.
    You’ll have to pass the full requested list of arguments, using default values like 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.

TypeScript IDE documentation
Example of a documentation based on TypeScript in VSCode

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

Single Object as an argument

Using a single object as a wrapper and using properties to pass arguments will resolve all the issues:

  • If a parameter is optional and you don’t want to define it, just omit the related property
  • You can define the properties in any order. It doesn’t matter.
  • with nicely named-properties, you’ll always have a clue of the purpose of a value
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 !

Calling the function with some extra parameters
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

Object destructuring

Object destructuring to the rescue
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:

  • No order.
  • Optional argument can be omitted.
  • Defining the attribute name allow you to understand the nature & purpose of the data.

TypeScript enhancement

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.

TypeScript IDE documentation

So TypeScript will help both the developer calling the function and a reviewer reading the code from the function itself

Shorthand property names

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.

Use it wisely

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.

Did you enjoy this post? Join Getaround's engineering team!
View openings