A javascript design pattern for options with default values?
Asked Answered
E

15

86
// opt_options is optional
function foo(a, b, opt_options) {
  // opt_c, opt_d, and opt_e are read from 'opt_options', only c and d have defaults
  var opt_c = 'default_for_c';
  var opt_d = 'default_for_d';
  var opt_e; // e has no default

  if (opt_options) {
    opt_c = opt_options.c || opt_c;
    opt_d = opt_options.d || opt_d;
    opt_e = opt_options.e;
  }
}

The above seems awfully verbose. What's a better way to handle argument options with default parameters?

Editorial answered 7/3, 2012 at 13:31 Comment(4)
jQuery and underscore both come with an extend method which handles this pretty well.Discretionary
@Discretionary - so, use extend and don't keep local variables for the options?Editorial
Actually, after a while I concluded that being verbose is a good thing when it comes to Javascript named arguments. Copying each argument to a variable at the start of the function is a good way to document what are the expected options and also lets you modify them without screwing with the original object.Sheriesherif
@missingno - I tend to agree, but I don't like each variable appearing twice. Check out my solution below.Editorial
E
-6

Now that I think about it, I kind of like this:

function foo(a, b, opt_options) {
  // Force opt_options to be an object
  opt_options = opt_options || {};

  // opt_c, opt_d, and opt_e are read from 'opt_options', only c and d have defaults
  var opt_c = 'default_for_c' || opt_options.c;
  var opt_d = 'default_for_d' || opt_options.d;
  var opt_e = opt_options.e; // e has no default
}
Editorial answered 7/3, 2012 at 13:50 Comment(8)
To continue our discussion... I also used to think like this, since I am a diehard code-duplication hater, but I changed my mind. I realized that I would often forget what options are accepted (and would have to search the function code to find out). Then II started adding comments to list the named arguments and then I moved on to just copying all the variables, treating all of them the same. While it is a little longer to type, it doesn't suffer from comment rot and it is more flexible (I can rename the variables inside the function, I can easily add and remove default values and so on...).Sheriesherif
@missingno - I'm not sure I follow. In this answer I do copy the variables into variables, and this won't suffer from code rot. I just don't repeat them twice.Editorial
I am terribly sorry, the lack of opt_a and opt_b completely threw me off :)Sheriesherif
Well, they're not _opt_ional :)Editorial
Should that not be var opt_c = opt_options.c || 'default_for_c'; Doesn't seem like opt_options.c would ever be reached since 'default_for_c' will evaluate as truthy.Ocana
@Ocana Another problem is what if opt_options.c is set to false, it would then be changed back to true, which is not intended. It's probably better to explicitly check for undefined in this case.Foulk
@RaySuelzer, agreed. something like: var opt_c = opt_options.c != null ? opt_options.c : 'default_for_c';Ocana
you'd want to use undefined since a null value must be explicitly declared as null in javascript.Foulk
S
92

This uses jQuery.extend but could be interchanged with an object merger from your library of choice or Object.assign in ES6.

function Module(options){
    var defaults = {
        color: 'red'
    };
    var actual = $.extend({}, defaults, options || {});
    console.info( actual.color );
}

var a = new Module();
// Red
var b = new Module( { color: 'blue' } );
// Blue

Edit: Now also in underscore or lodash!

function Module(options){
    var actual = _.defaults(options || {}, {
         color: 'red'
    });
    console.info( actual.color );
}

var a = new Module();
// Red
var b = new Module( { color: 'blue' } );
// Blue

In Javascript ES6 you can use Object.assign:

function Module(options = {}){
    let defaults = {
        color: 'red'
    };
    let actual = Object.assign({}, defaults, options);
    console.info( actual.color );
}
Sikes answered 7/3, 2012 at 13:50 Comment(6)
The naming is kind of confusing ... you don't really want defaults.color, you want the actual option.Editorial
I did a minor style edit, changed 'color' into color - I think it's more idiomatic js, no?Editorial
Edited the naming to make it a bit more apparent.Sikes
Late to the party, but the problem with object.assign is that it will copy the entire object in from "options". So if you have a deep nesting structure, it will not merge those options, it will overwrite them.Zooid
@Zooid thats a good point. Deep nesting seems a bit out of scope of the original question though.Sikes
FWIW Lodash also has a defaultsDeep function I ended up using. I really wish default parameters would work many levels deep (config.extraPropsForFields.defaultPropsForTextFields = { hasFloatingLabel: true; placeholder: ' ' }Extraversion
S
71

Using the ES2018 spread syntax for object properties:

const defaults = { a: 1, b: 2 };

const ƒ = (given = {}) => {
  const options = { ...defaults, ...given };
  console.log(options);
};

Using ES6/ES2015 features, several more options are available.

Using destructuring assignment:

const { a = 1, b = 2 } = options;

You can also use destructuring function parameters:

const ƒ = ({a = 1, b = 2, c = 3} = {}) => {
   console.log({ a, b, c });
};

Using Object.assign:

options = Object.assign({}, defaults, options);

No dependencies!

Schaper answered 20/7, 2016 at 3:51 Comment(2)
this pattern is great, saves me a lot of real estate. Though it should be noted that the assignment is shallow, not deepBirdlime
Be careful! if your options input has a known key (e.g. a) mapped to undefined, you won't pick up the value for that property from your defaultsPolychromatic
H
20

To get default options without additional dependencies, I use the following pattern:

var my_function = function (arg1, arg2, options) {
    options = options || {};
    options.opt_a = options.hasOwnProperty('opt_a') ? options.opt_a : 'default_opt_a';
    options.opt_b = options.hasOwnProperty('opt_b') ? options.opt_b : 'default_opt_b';
    options.opt_c = options.hasOwnProperty('opt_c') ? options.opt_c : 'default_opt_b';


    // perform operation using options.opt_a, options.opt_b, etc.
};

Although a bit verbose, I find it easy to read, add/remove options and add defaults. When there are LOTS of options, a slightly more compact version is:

var my_function = function (arg1, arg2, options) {
    var default_options = {
        opt_a: 'default_opt_a',
        opt_b: 'default_opt_b',
        opt_c: 'default_opt_c'};

    options = options || {};
    for (var opt in default_options)
        if (default_options.hasOwnProperty(opt) && !options.hasOwnProperty(opt))
            options[opt] = default_options[opt];

    // perform operation using options.opt_a, options.opt_b, etc.
};
Hestia answered 22/10, 2013 at 17:59 Comment(1)
Also, I would like to point out that as of Oct 22 the accepted answer by ripper234 doesn't work correctly. In the code example opt_c and opt_d always have default_for_c and default_for_d, even if other values were present in the options object. Changing the ordering of the parameters to the logical OR fixes this (a || b vs b || a).Hestia
S
11

And the more compact jQuery version:

function func(opts) {
    opts = $.extend({
        a: 1,
        b: 2
    }, opts);

    console.log(opts);
}

func();            // Object {a: 1, b: 2} 
func({b: 'new'});  // Object {a: 1, b: "new"} 
Stubstad answered 19/1, 2014 at 5:14 Comment(1)
Flaw with that is that your code now depends on jQuery to operate and if that is the only reason for using jQuery. You're adding a lot of unnecessary code on the client side by including jQuery.Spermiogenesis
D
4

Using ES6 Spread Operator without using external libraries

function Example(opts) {
   let defaults = { foo: 1, bar: 2 }
   opts = { ...defaults, ...(opts || {}) }
   console.log(opts);
}

Example({ bar: 3, baz: 4 })

// { foo: 1, bar: 3, baz: 4 }
Darsie answered 24/7, 2020 at 21:37 Comment(0)
S
2

If you need to do this in many consecutive functions, a way to standardize the process and speed it up is:

function setOpts (standard, user) {
  if (typeof user === 'object' {
    for (var key in user) {
      standard[key] = user[key];
    }
  }
}

Then you can just define your functions simply like this:

var example = function (options) {
  var opts = {
    a: 1,
    b: 2,
    c:3
  };
  setOpts(opts, options);
}

This way you only define one options object inside the function, which contains the default values.

If you want to put an extra check to avoid prototype inheritance, the first function can be:

function setOpts (standard, user) {
  if (typeof user === 'object') {
    Object.keys(user).forEach(function (key) {
      standard[key] = user[key];
    });
  }
}

The latter case is not supported for: IE < 9, Chrome < 5, Firefox < 4, Safari < 5

(you can check the compatibility table here)


Finally ECMAScript 6 will bring us the best way to do this: default parameters.

It will take a few months though before this is widely supported across browsers.

Sugared answered 20/2, 2015 at 2:31 Comment(1)
"t will take a few months though before this is widely supported across browsers.". I like your optimism. :)Sikes
D
2

If you have access to ES6 with a stage 4 proposal (such as with Babel) you can accomplish this with spread and destructuring assignment.

const defaultPrintOptions = {
  fontName: "times",
  fontStyle: "normal",
  fontSize: 10,
  align: "left"
};

// Setting the null default isn't necessary but
// makes it clear that the parameter is optional.
// Could use {} but would create a new object
// each time the function is called.
function print(text, options = null) {
  let {
    fontName,
    fontStyle,
    fontSize,
    align
  } = {
    ...defaultPrintOptions,
    ...options
  };

  console.log(text, fontName, fontStyle, fontSize, align);
}

print("All defaults:");
print("Override some:", {
  fontStyle: "italic",
  align: "center"
});
print("Override all:", {
  fontName: "courier",
  fontStyle: "italic",
  fontSize: 24,
  align: "right"
});

This also works (but may create more objects):

function myFunction({ 
  text = "", 
  line = 0, 
  truncate = 100 
} = {}) {
  console.log(text, line, truncate);
}

(latter example from David Walsh - @wprl's answer also mentions this)

Debenture answered 1/1, 2019 at 14:33 Comment(0)
G
2

Here is a simple and clean approach, hopefully, this helps someone:

function example(url, {title = false, check = false, wait = false} = {}){
  console.log('url: ', URL);
  console.log('title: ', title);
  console.log('check: ', check);
  console.log('wait: ', wait);
}

example('https://example.com', {wait: 20})

Here is the output of the code above:

url:  https://example.com
title:  false
check:  false
wait:  20
Gujral answered 1/4, 2020 at 20:2 Comment(1)
This is the best. Also in TypeScript, it would be something like this: {title = false, check = false, wait = false}: {title: false | string, check: boolean, wait: false | number} = {}Tarantella
I
1
      var mergeOptions = function mergeOptions(userOptions) {

        // Default options
        var options = {
            height: "100px",
            width: "100px" ,
            color: "blue"
        }

        if (userOptions) {
            Object.keys(userOptions).forEach(function (key) {
                options[key] = userOptions[key]
            })
        }

        return options;
    }
Ineradicable answered 17/7, 2019 at 1:41 Comment(0)
J
1

After seeing all solutions, I prefer the way as following:

function example(firstArgument, optional = {}) {
  const {
    a = 123, // default value
    b,
  } = optional;
  console.log(firstArgument, a, b);
}

And this way support multiple syntax to call:

example(); // undefined 123 undefined
example("test"); // test 123 undefined
example("test", { a: 456 }); // test 456 undefined
example("test", { b: 456 }); // test 123 456
example("test", { a: 456, b: 123 }); // test 456 123
Jailbird answered 23/12, 2022 at 7:42 Comment(0)
O
0

Although Object.assign is pretty straight way to merge options with defaults it has some disadvantages:

  1. if you want to set conditional options with ternary operator - it will overwrite defaults even for undefined values:

    const options = {
      logging: isProduction ? 'none' : undefined
    };
    const defaults = {
      logging: 'verbose'
    }
    Object.assign({}, defaults, options); // {logging: undefined} !
    
  2. if you provide incorrect option name - you will not be warned:

    const options = {
      loging: 'none' // typo
    };
    const defaults = {
      logging: 'verbose'
    }
    Object.assign({}, defaults, options); // {logging: 'verbose', loging: 'none'} !
    

To cover these cases I've created tiny flat-options package.
It does not overwrite defaults for undefined values:

const options = {
  logging: isProduction ? 'none' : undefined
};
const defaults = {
  logging: 'verbose'
}
flatOptions(options, defaults); // {logging: 'verbose'}

and warns for incorrect option names:

const options = {
  loging: 'none' // typo
};
const defaults = {
  logging: 'verbose'
}
flatOptions(options, defaults); // throws "Unknown option: loging."

Hope this helps!

Ornamented answered 17/10, 2017 at 11:8 Comment(0)
E
0

There is a new javascript syntax for easily setting defaults, logical assignment operators:

// Super crazy this:
staticConfig.defaultPropsForFoo = 
  staticConfig.defaultPropsForFoo || {
    myDefault: 'props'
  }
// turns into:
staticConfig.defaultPropsForFoo ||= { myDefault: 'props' }

Also works with nullish operator if you prefer stricter boolean semantics:

staticConfig.defaultPropsForFoo ??= { myDefault: 'props' }

(Arguably we should always use ??= version but it's also pretty new)

Also, I use default params all the time, but this syntax works inside of any destructuring assignment:

const {
  defaultPropsForFoo = { myDefault: 'props' },
  ...restConfig
} = staticConfig
Extraversion answered 30/12, 2020 at 4:43 Comment(0)
A
0

This is a very old question, and the answers aren't updated as per modern destructuring. If using Typescript or Babel:

function test({ a = 2, b = 2 } = {}) {
  console.log(a + b)
}

test() // 4
test({ a: 3 }) // 5

Using = provides the default arguments for the properties, and the last = makes sure an empty object is set with the default property values if no argument is passed in.

Aspinwall answered 20/6, 2024 at 8:27 Comment(0)
H
-1

I think you're looking for something like this (sorry for the late reply):

function foo(a, b, options) { 
    this.defaults = {
        x: 48, 
        y: 72,
        z: 35
    };
    for (var i in this.defaults) {
        if (options[i] != "undefined") { this.defaults[i] = options[i]; }
    }
    // more code...
}

edit: apologies, grabbed this from some old code... You should make sure to use the hasOwnProperty() method to make sure you don't iterate over everything on function.prototype

https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/hasOwnProperty

Halona answered 4/3, 2013 at 21:26 Comment(2)
There's a typo here: you're checking if the options are the string "undefined" instead of really undefined (you probably left out a typeof, but options[i] !== undefined is good too if you're willing to assume that some bozo didn't redefine undefined).Orpah
For a working version of this pseudo-code, see the answer by Rick Deckard.Terraqueous
E
-6

Now that I think about it, I kind of like this:

function foo(a, b, opt_options) {
  // Force opt_options to be an object
  opt_options = opt_options || {};

  // opt_c, opt_d, and opt_e are read from 'opt_options', only c and d have defaults
  var opt_c = 'default_for_c' || opt_options.c;
  var opt_d = 'default_for_d' || opt_options.d;
  var opt_e = opt_options.e; // e has no default
}
Editorial answered 7/3, 2012 at 13:50 Comment(8)
To continue our discussion... I also used to think like this, since I am a diehard code-duplication hater, but I changed my mind. I realized that I would often forget what options are accepted (and would have to search the function code to find out). Then II started adding comments to list the named arguments and then I moved on to just copying all the variables, treating all of them the same. While it is a little longer to type, it doesn't suffer from comment rot and it is more flexible (I can rename the variables inside the function, I can easily add and remove default values and so on...).Sheriesherif
@missingno - I'm not sure I follow. In this answer I do copy the variables into variables, and this won't suffer from code rot. I just don't repeat them twice.Editorial
I am terribly sorry, the lack of opt_a and opt_b completely threw me off :)Sheriesherif
Well, they're not _opt_ional :)Editorial
Should that not be var opt_c = opt_options.c || 'default_for_c'; Doesn't seem like opt_options.c would ever be reached since 'default_for_c' will evaluate as truthy.Ocana
@Ocana Another problem is what if opt_options.c is set to false, it would then be changed back to true, which is not intended. It's probably better to explicitly check for undefined in this case.Foulk
@RaySuelzer, agreed. something like: var opt_c = opt_options.c != null ? opt_options.c : 'default_for_c';Ocana
you'd want to use undefined since a null value must be explicitly declared as null in javascript.Foulk

© 2022 - 2025 — McMap. All rights reserved.