Initializing a destructured argument

Last updated August 31, 2021 by Jacob Paris

Destructuring arguments is a pattern that shows up a lot in modern javascript, and it's common in a lot of React components.

js
1function useLocalStorageState(
2 key,
3 defaultValue = '',
4 {
5 serialize = JSON.stringify,
6 deserialize = JSON.parse
7 } = {},
8)
9

But why do we need to assign to an empty object?

js
1{
2 serialize = JSON.stringify,
3 deserialize = JSON.parse
4} = {},
5// assignment to an empty object
6

Why is it not just like this?

js
1/* prettier-ignore */
2{
3 serialize = JSON.stringify,
4 deserialize = JSON.parse
5} // no assignment
6

If you try that, you'll end up with an error. Depending on the different ways you try this, you can get one of several errors:

TypeError: Cannot read property 'serialize' of undefined
TypeError: Cannot destructure property 'serialize' of 'undefined' as it is undefined.
TypeError: Cannot destructure property 'serialize' of (intermediate value) as it is undefined.

To show why that happens, here's an example of two functions.

js
1function log(input) {
2 console.log(input)
3}
4
5log(5) // outputs 5
6log() // outputs undefined
7
js
1function log(input = 10) {
2 console.log(input)
3}
4
5log(5) // outputs 5
6log() // outputs 10
7

Do you follow this so far?

The second example was able to output 10 without passing an argument into log()

Function parameters can have defaults, and those are set with the = sign. Default arguments (sometimes called optional arguments or optional parameters) allow you to call the function with nothing for that argument.

js
1function useLocalStorageState(key, defaultValue = '', handlers = {}) {}
2

With this function, if you don't pass in a default value it'll default to an empty string, and if you don't pass in a handlers object it'll default to an empty object.

Assume handlers contains a serialize and a deserialize property, we can optionally set them to the JSON methods

js
1function useLocalStorageState(
2 key,
3 defaultValue = '',
4 handlers = {}
5) {
6 if (!handlers.serialize) {
7 handlers.serialize = JSON.stringify
8 }
9
10 if (!handlers.deserialize) {
11 handlers.deserialize = JSON.parse
12 }
13

Or we could destructure them right in the argument

js
1function useLocalStorageState(
2 key,
3 defaultValue = '',
4 { serialize, deserialize } = {}
5) {
6 if (!serialize) {
7 serialize = JSON.stringify
8 }
9
10 if (!deserialize) {
11 deserialize = JSON.parse
12 }
13

But when we're destructuring, we can assign defaults there too, so no need to do it within the function And then you're pretty much at what you were seeing

js
1function useLocalStorageState(
2 key,
3 defaultValue = '',
4 {
5 serialize = JSON.stringify,
6 deserialize = JSON.parse
7 } = {}
8) {
9
js
1// This version uses custom serialize and deserialize functions
2useLocalStorage(key, value, {
3 serialize: myCustomSerializer,
4 deserialize: myCustomDeserializer,
5})
6
7// This version uses the default deserializer... but a custom serializer
8useLocalStorage(key, value, {serialize: myCustomSerializer})
9
10// This version uses the default serializer... but a custom deserializer
11useLocalStorage(key, value, {deserialize: myCustomDeserializer})
12
13// This version uses the default for both!
14useLocalStorage(key, value)
15

If you leave out the = {}, it is no longer an optional argument and you must supply a value

useLocalStorage(key, value, {})

If you don't supply a value, javascript will interpret it as undefined

useLocalStorage(key, value, undefined)

which means the third argument will try to destructure undefined and crash

// This will throw an error because you can only destructure objects and arrays
{ serialize, deserialize } = undefined