# How to accept optional and reordered function arguments

> Match a call's arguments against a pattern of names and types, so one function accepts several shapes without a chain of typeof checks.

Source: https://voxgig.com/howto/normalise-optional-function-arguments

- Audience: library-maintainer
- Level: intermediate
- Languages: typescript, javascript
- Verified: 2026-09-14
- Published: 2026-09-14
- Updated: 2026-09-24

## Short answer

Describe the arguments as a pattern of names and types, and match the call against it left to right. An argument that does not fit the next slot is tried against the one after, so an optional argument can be left out from the middle. A required slot that nothing fills raises an error naming the slot and what it expected.

---
## You will need

Node 22 or later, and a function whose callers pass arguments in more than one shape. The matching
rests on distinguishing the runtime types properly, which means treating `null` and an array as
their own kinds rather than as
[objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof).[^1]
[Array.isArray](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray)
is the check for the second.

Voxgig maintains norma. This page compares that idea with overloads, an options object, and the
checks you would otherwise write by hand.

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [A pattern matcher such as norma](https://github.com/rjrodger/norma) | Several optional arguments, and calls already written in several shapes | A small dependency, and a pattern language to learn and document | The function has two arguments and one is optional |
| [A single options object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) | A new API, where you can choose the calling convention outright | A breaking change for existing callers, and a heavier call for the short case | You cannot change how existing code calls the function |
| [Hand-written typeof checks](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof) | One or two shapes, where the branches fit on a screen | Growth with every optional argument, since each one doubles the branches | A third optional argument arrives |
| [TypeScript overloads](https://www.typescriptlang.org/docs/handbook/2/functions.html#overload-signatures) | You ship types and want the editor to show each shape separately | Types only, so the runtime implementation still has to sort the arguments out | Your callers are untyped, which is most published libraries |

The options object is what to reach for in a function you are designing today, and it is not
available for one that has shipped. A published function with four positional arguments has callers
you cannot see, and changing the convention breaks all of them.

Overloads and a matcher solve different halves. Overloads tell the compiler and the editor what
shapes exist; the matcher is what the code does at runtime when a caller ignores both. Shipping both
is common, and only one of them is enforcement.[^2]

## Describe the shapes in one line

The pattern carries the names, the types, the optionality, and the defaults.

```js title="client.mjs"
export function listMeters(...args) {
  return normalize('url:string options:object? limit:number=50 done:function?', args)
}
```

Reading it left to right is what makes an omitted middle argument work. A number in the second
position does not fit `options:object?`, so the matcher moves on and tries `limit:number`, and the
caller gets what they meant.

Keep the pattern beside the function rather than in a shared table. It is documentation as much as
code, and a reader of the function should not have to look elsewhere to learn what it takes. A
pattern in a shared constants file is a signature nobody can see from the call site.

## Refuse what does not fit

An argument nothing can hold is a caller mistake, and saying so early is the point.

```js title="normalize.mjs"
if (at < args.length) throw new TypeError(`unexpected extra argument of type ${typeOf(args[at])}`)
return out
```

Without that line, a typo such as passing a string where a function belongs is silently dropped, and
the failure surfaces much later as a callback that never fires.

Name the slot in the message. `url: expected string, got object` sends a caller to the right
argument, while a failure on a property access inside your function sends them into your source.

## Check it worked

Seven calls: five of them valid, and two of them mistakes.

```bash
node demo.mjs
```

```text output
("https://api.example.com")                       url="https://api.example.com" limit=50
("https://api.example.com", {"retry":true})       url="https://api.example.com" options={"retry":true} limit=50
("https://api.example.com", 100)                  url="https://api.example.com" limit=100
("https://api.example.com", {"retry":true}, 100)  url="https://api.example.com" options={"retry":true} limit=100
("https://api.example.com", fn)                   url="https://api.example.com" limit=50 done=fn
({"retry":true})                                  TypeError: url: expected string, got object
("https://api.example.com", "extra")              TypeError: unexpected extra argument of type string
```

Rows three and five are the ones a chain of checks gets wrong. A number in second position skips the
options slot, and a function in second position skips both options and limit, and in each case the
default still applies. Every valid call ends with the same named arguments regardless of how many
were passed.

The last two rows fail at the call rather than somewhere inside. That is worth as much as the
flexibility: a library that accepts anything and fails later is harder to use than one that says no.

The pattern is also the shortest documentation of the signature anyone will write. From its four
names, four types, and one default, a reader knows every shape the function takes without reading
the body.

```bash
node --test normalize.test.mjs
```

```text output
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 117.671513
```

## When it goes wrong

Two optional slots share a type. The matcher fills the first and the caller meant the second, and it
cannot know. Give one of them a distinct type, or move it into the options object. Two optional
strings in a row is the case that cannot be rescued by any matcher.

`null` fills an object slot. The type check used `typeof`, which reports `object` for null. Treat
null as its own type, which the sample does.

An array fills an object slot. Same cause, same fix. Arrays are objects to `typeof` and are never
what an options slot means. A list of ids passed where options belong is the call that finds this.

The pattern and the documentation disagree. Two descriptions of one signature. Generate the
documented signature from the pattern, or at least test that the examples in the documentation still
match it.

## When not to do this

Do not use a matcher on a function with two arguments. A single `if` is clearer than a pattern, and
a dependency for that is not worth explaining to the next maintainer.

Do not use it to avoid designing a signature. Accepting six shapes because callers asked for each of
them produces a function nobody can describe, and every shape is a promise you now maintain forever.

Do not reach for norma, or any matcher, on a new API. Take the options object while you still can,
and keep the matcher for functions whose calling convention is already in the wild.[^3]

## Related how-tos

- [Merge nested config objects with predictable precedence](/howto/merge-nested-config-objects-with-precedence)

- [Read a value at a nested path without null checks](/howto/read-a-nested-value-by-path)

## Last verified

Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.

[^1]: `typeof null` answers `"object"` for a reason
[MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof#typeof_null)
sets out with some care. The first JavaScript engine stored each value as a type tag and a value.
The tag for objects was 0, and null was the null pointer, which is also 0. A fix was proposed for
ECMAScript, as an opt-in, and rejected. The behavior stands since the beginning of the language, as
the page puts it, and it is at least dependable.

[^2]: The [TypeScript
handbook](https://www.typescriptlang.org/docs/handbook/2/functions.html#overload-signatures)
documents overloads and then advises against them. Its worked example rewrites two overloads as one
signature with a union type and calls the result much better. It then closes with a rule: always
prefer parameters with union types instead of overloads when possible. The feature is fully
specified, fully supported, and recommended against by its own manual, which is a position a manual
can hold and a compiler cannot.

[^3]: norma has been at this since February 2014, when its first version reached
[npm](https://www.npmjs.com/package/norma), and sixteen versions have followed. The registry
describes it as a function argument organizer, and [its README](https://github.com/rjrodger/norma)
opens by observing that optional arguments make an API nicer. The README says nothing about what
they cost the maintainer, which is the subject of this section.