Kula.blog

Learn JS. Destructuring 3 — Options Object JS Pattern

I'm assuming you've read the previous lesson Learn JS. Destructuring 2 — Assignment and Default Value

Let's use drawRect as an example for this lesson. It's a function that, given a canvas context, color, position, and size draws a rectangle.

// const canvas = document.getElementById('drawingBoard')
// const context = canvas.getContext('2d')

drawRect(context, 'red', 10, 10, 100, 150)

function drawRect(context, color, x, y, width, height) {
context.fillStyle = color;
context.fillRect(x, y, width, height);
}

It may not be visible to you yet, but drawRect is harder to use than it should be. If you import this function in some other module, then it's impossible to know what the numbers mean.

import {drawRect} from "./drawingFunctions";

drawRect(context, 'red', 10, 10, 100, 150)

If you want to move the rectangle by 5 pixels down, which of the 10 would you change? Does 100 set the width or height? Even if you still remember right now, can you be sure to remember it in a week or a couple of months?

The second problem comes when you want to make your function easier to use by adding default parameters;

function drawRect(context,
color = "red",
x = 0,
y = 0,
width = 50,
height = 50) {
context.fillStyle = color;
context.fillRect(x, y, width, height);
}

It will get easier to use if you want to draw squares 50x50 pixels like:

drawRect(context, "green", 100, 20);

Unfortunately, if we want to change the dimensions and use other default values at the same time, then we have to pass undefined to keep the order of parameters. For example, to draw a red rectangle in the top left corner with the height of 100px, we need to pass undefined five times.

drawRect(context,
undefined /* color: red */,
undefined /* x: 0 */,
undefined /* y: 0 */,
undefined /* height: 50 */,
100);

In real life, you will see either only a call without all those comments:

drawRect(context, undefined, undefined, undefined, undefined, 100);

Or someone will pass all the arguments to make it easier to read and negate all your work with setting up the default parameters:

drawRect(context, "red", 0, 0, 50, 100);

Destructuring to the rescue! #

You could argue that what's giving us most value is the Options Object Pattern. But, without destructuring, it would look a lot worse, and that would negate some of its benefits. It's also easier to refactor your parameters into the new Options Object by taking advantage of destructuring.

Destructuring parameters has the same restrictions as object destructuring. The most important one is that destructuring needs an object—even an empty one. If you forget to pass one, then even if you set up a default parameter, you will only get an Error.

function requireOptions({ test = "tricky" }) {
console.log(test)
}

requireOptions(); // won't work!
// ERROR: Cannot destructure property `test` of 'undefined' or 'null'.

requireOptions({});
// logs "tricky"

If you forget to pass an empty object, then it won't work. Fortunately, we can add a default parameter with an empty object to work around it. Look for " = {}" at the end of the line.

- function requireOptions({ test = "tricky" }) {
+ function requireOptions({ test = "tricky" } = {}) {

(If it's confusing, then take a look at the previous post: Learn JS. Destructuring 2 — Assignment and Default Value.)

function requireOptions({ test = "tricky" } = {}) {
console.log(test)
}

requireOptions();
// logs "tricky"

Final example #

It's time to rewrite our function and use destructuring on the options object.

function drawRect(context, {
color = "red",
x = 0,
y = 0,
width = 50,
height = 50 } = {}) {
context.fillStyle = color;
context.fillRect(x, y, width, height);
}

In this case, context doesn't have any default value, so I've decided to keep it separate. Let's draw some rectangles to see the difference.

- drawRect(context, "green", 100, 20);
+ drawRect(context, { color: "green", x: 100, y: 20 });

The first example doesn't change much, but it gives us a more readable code. It's obvious now that 100 sets x and that 20 sets y. The next example shows clearly the advantages of this approach.

- drawRect(context,
- undefined /* color: red */,
- undefined /* x: 0 */,
- undefined /* y: 0 */,
- undefined /* height: 50 */,
- 100);
+ drawRect(context, { width: 100 });

Our code got a lot shorter and easier to read. It should be obvious how options pattern improves your code.

If you feel that it's unfair comparison with all those comments, then the same thing could have been written without them:

- drawRect(context, undefined, undefined, undefined, undefined, 100);
+ drawRect(context, { width: 100 });

Shorter and easier to understand.

It's not so apparent in the next example, so please remember that all those values are the same as default parameter values. It's a workaround for all those undefined you've seen in the previous example.

- drawRect(context, "red", 0, 0, 50, 100);
+ drawRect(context, { width: 100 });

In this example, the most important is how you can prevent default parameter values form being copy-pasted all over your codebase. It's a lot more consistent and more comfortable to maintain when you use the options object pattern.


Please let me know if you have any questions.

In the next lesson, I'll explain what to do if the name of the property clashes with one of the existing variables.

If you like this newsletter, then please share it with your friends: https://kula.blog/learnjs/


← Home