Named default ES6 parameter without destructuration - javascript

I'm trying to default options in ES7 using babel. Here is what I can do:
class Foo {
constructor({key='value', foo='bar', answer=42}) {
this.key = key;
this.foo = foo;
this.number = number;
}
}
This might work for this example, but I would like to know how can I assign for very large config objects; here is an example of what I wanna do:
class Foo {
constructor(opts = {key='value', foo='bar', answer=42}) {
this.opts = opts;
}
}
However this does not compile. I tried to do it like this:
class Foo {
constructor(opts = {key:'value', foo:'bar', answer:42}) {
this.opts = opts;
}
}
But then it replaces the whole object, like this:
let foo = new Foo({key: 'foobar'});
console.log(foo.opts);
// {key: 'foobar'} is what is displayed
// When I want {key: 'foobar', foo:'bar', answer:42}

I don't think you can do this with ES6 optional parameters (object as a parameter with optional keys), because when you call the constructor, it's a new object with a new reference. That's because it's being replaced.
But, as a suggestion, if you want to handle a large options object, one common approach is store somewhere a default options Object and merge the object with the one passed when you instantiate it.
Something like that:
class Foo {
constructor(opts) {
this.opts = Object.assign({}, Foo.defaultOptions, opts)
console.log(this.opts)
}
}
Foo.defaultOptions = {
key: 'value',
foo: 'bar',
answer: 42
}
let foo = new Foo({key: 'another value'})
//{ key: 'another value', foo: 'bar', answer: 42 }
You can merge with Object.assign (be aware that it does not perform deep merging - nested objects are replaced).
Or, if you want to declare your default options Object as a class variable (not at the end, after class declaration, or inside constructor), as you're using babel, you can use this plugin and do this:
class Foo {
defaultOptions = {
key: 'value',
foo: 'bar',
answer: 42
}
constructor(opts) {
this.opts = Object.assign({}, this.defaultOptions, opts)
console.log(this.opts)
}
}
It's more readable.

It is
class Foo {
constructor({key='value', foo='bar', answer=42} = {}) {
...
}
}
It is ES6 destructuring feature, not specific to ECMAScript 7 (ECMAScript Next) proposals.
Without destructuring it is usually done with object cloning/merging, Object.assign comes to help:
class Foo {
constructor(opts = {}) {
this.opts = Object.assign({
key: 'value',
foo: 'bar',
answer: 42
}, opts);
}
}

Related

How to overwrite default (object) values when destructuring a nested JavaScript object

I'm trying to create a JavaScript class which takes an object as its only argument. The object properties shall be merged with some defaults which are defined in the class itself (and then be used as class fields). So far I'm using object destructuring to achieve what I want to do – which works great when the object is only one level deep. As soon as I use a nested object I'm not able to overwrite "parent" properties (without passing a nested object) anymore.
Is object destructuring even the right approach to what I want to do?
The class itself looks like this
class Class {
constructor( args = {} ) {
this.prop1 = {};
( {
type: this.type = 'default',
prop1: {
value1: this.prop1.value1 = 'one',
value2: this.prop1.value2 = 'two',
value3: this.prop1.value3 = 'three'
} = this.prop1
} = args );
}
}
Expected
When creating a new Class with the following call
new Class( { type: 'myclass', prop1: { value1: 1 } } );
the class fields are assigned properly and the class has the following structure:
{
type: 'myclass',
prop1: {
value1: 1,
value2: 'two',
value3: 'three'
}
}
Which is perfect and exactly what I want to achieve.
Unexpected
It gets tricky when I want to overwrite some fields not with the expected values / structure. A new class creation like the following "fails" as the input values are overwritten with the default values.
The following call
new Class( { type: 'myclass', prop1: 'myProp1' } );
results in the following structure:
{
type: 'myclass',
prop1: {
value1: 'one',
value2: 'two',
value3: 'three'
}
}
Which is not what I want it to be / what I expected.
I would have expected the following structure:
{
type: 'myclass',
prop1: 'myProp1'
}
Is there any way to generate the class in the described way – using object destructuring – or is it just not the right use case?
Thanks in advance!
I would avoid having a property that can be either a string or an object, but if you really need it (not just as input but also as the resulting class property), you can achieve this by destructuring twice:
class Class {
constructor( args = {} ) {
( {
type: this.type = 'default',
prop1: this.prop1 = {},
} = args );
if (typeof this.prop1 == 'object') {
( {
value1: this.prop1.value1 = 'one',
value2: this.prop1.value2 = 'two',
value3: this.prop1.value3 = 'three',
} = this.prop1 );
}
}
}
(Notice that this does mutate the args.prop1 if you pass an object!)
I'd avoid destructuring onto object properties though, it's fairly uncommon (unknown) and doesn't look very nice. I'd rather write
class Class {
constructor(args) {
this.type = args?.type ?? 'default',
this.prop1 = args?.prop1 ?? {};
if (typeof this.prop1 == 'object') {
this.prop1.value1 ??= 'one',
this.prop1.value2 ??= 'two',
this.prop1.value3 ??= 'three',
}
}
}
(This still does mutate the args.prop1 if you pass an object! Also it treats null values differently)
or if you really want to use destructuring,
class Class {
constructor({type = 'default', prop1 = {}} = {}) {
this.type = type;
if (typeof prop1 == 'object') {
const {value1 = 'one', value2 = 'two', value3 = 'three'} = prop1;
this.prop1 = {value1, value2, value3};
} else {
this.prop1 = prop1;
}
}
}
(This does always create a new this.prop1 object that is distinct from args.prop1)
If I was reviewing your code, I would make you change it. I've never seen destructuring used like that to set values on another object. It's very strange and smelly... but I see what you're trying to do.
In the most simple of cases, I suggest using Object.assign in this scenario. This allows you to merge multiple objects in the way you want (notice how easy it is to read):
const DEFAULTS = {
type: 'default',
prop1: {
value1: 'one',
value2: 'two',
value3: 'three'
}
}
class SomeClass {
constructor( args = {} ) {
Object.assign(this, DEFAULTS, args);
}
}
This will only merge top-level properties. If you want to merge deeply nested objects too, you will need to do that by hand, or use a tool like deepmerge. Here's an example of doing it by hand (while less easy to read, it's still pretty normal code):
class SomeClass {
constructor( args = {} ) {
Object.assign(this, {
...DEFAULTS,
...args,
prop1: typeof args.prop1 === 'string' ? args.prop1 : {
...DEFAULTS.prop1,
...args.prop1
}
});
}
}

How to merge options object passed in constructor as properties

I want to create a class that takes an options object as it's argument in its constructor. Each of the member of option object should become member of the class instance.
const __defaults = {
isDisabled = false,
caption: undefined
}
class Button {
constructor(options) {
this.caption = options.caption || __defaults.caption;
this.isDisabled = options.isDisabled || __defaults.disabled;
}
}
Is there a better way to handle this like spread operator?
this.config = { ...options, ...__defaultOptions };
The only problem is I can't assign directly to this using spread operator. That would be an invalid assignment.
this = { ...options, ...__defaultOptions };
I want to create all properties directly inside class and not within a config property in the instance. That way when I initialize my button in following manner...
const registerButton = new Button({ isDisabled: true });
I can read property just like this:
console.log(registerButton.isDisabled);
and not like this...
console.log(registerButton.config.isDisabled);
Former approach is more verbose and readable.
Use Object.assign()
class Foo{
constructor(options)
{
Object.assign(this,options);
}
}
let foo = new Foo({a:1,b:2,c:3});
console.log(foo);
Object.assign can assign the merged properties onto the instance.
constructor(options) {
Object.assign(
this,
__defaultOptions,
options
);
}
You can create a temp object i.e obj and then loop over the object keys and then add property on this
const __defaults = {
isDisabled: false,
caption: undefined,
};
class Button {
constructor(options) {
const obj = { ...options, ...__defaults };
Object.keys(obj).forEach((k) => (this[k] = obj[k]));
}
}
const registerButton = new Button({
isDisabled: true
});
console.log(registerButton);

Problem with default parameter and object

I've got a problem with default parameters in Javascript.
I have a function like this:
function search(filterOptions = {
foo: 'bar',
foo2: 'bar2'
}) {
...
}
When I call search() without arguments, filterOptions is set to {foo: 'bar', foo2: 'bar'},
but when I call search({ foo: 'something' }), foo2 is undefined.
I cannot separate filterOptions into several arguments because options are independent.
How can i make foo2 take its default value anyway (and cleanly)?
(I'm on nodejs)
Thank you!
You could define the defaults within the function and use the spread syntax to combine the two objects, which will override the defaults where applicable.
function search(filterOptions) {
const defaults = { foo: 'foo', foo2: 'bar' };
filterOptions = {...defaults,...filterOptions};
console.log(filterOptions);
}
search({foo: 'something'});
You can provide default values in the parameter list:
function search({ foo = "bar", foo2 = "bar2"} = {}) {
console.log("foo is " + foo + ", foo2 is " + foo2);
}
console.log(search());
console.log(search({ foo: "broccoli" }));
console.log(search({ foo: "my foo", foo2: "my foo2" }));
The = {} at the end is to handle the case when the function is called with no parameters.
You can define the default variable with an if statement:
function search(arr) {
if(arr.bar === undefined) {
arr.bar = "bar1";
}
//do whatever
}
The use of === is to make sure that the if does not run if bar is set to "undefined" (a string).

Generic reading of arguments from multiple constructor calls

Follow-up question to Read arguments from constructor call:
The accepted solution allows me to get arguments passed into a constructor by defining a wrapper class that captures and exposes the arguments, but this leaves me with the problem of having n wrappers for n constructors.
Is there a way to have 1 function/wrapper/whatever that could work for any number of constructors?
I'll reiterate that I'm pursing this technique specifically to test Webpack plugin configuration, and I'd like to avoid having a separate wrapper for each plugin that I need to test.
Looking for something along the lines of
// ------------------------------------------------------------ a wrapper function?
const someWrapper = () => { /* ... */ }
const plugin1 = new Plugin({ a: 'value' })
const plugin2 = new Plugin2(arg1, arg2, { b: 'anotherValue '})
someWrapper(plugin1).args === [{ a: 'value' }]
someWrapper(plugin2).args === [arg1, arg2, { b: 'anotherValue' }]
// --------------------------------------------------------------- a wrapper class?
class Wrapper { /* ... */ }
const plugin1 = new Wrapper(Plugin, [{ a: 'value' }])
const plugin2 = new Wrapper(Plugin2, [arg1, arg2, { b: 'anotherValue '}])
plugin1.args === [{ a: 'value' }]
plugin2.args === [arg1, arg2, { b: 'anotherValue '}]
// problem with above is the wrapper is being passed to Webpack, not the underlying
// plugin; not sure yet if this would cause webpack to break or not actually
// execute the plugin as intended with a vanilla config
// ---------------------------------------------------------------- something else?
Yes, you can create generic wrapper which will add args property to instance of any passed constructor:
class Plugin {
constructor (arg1, arg2) {
this.arg1 = arg1
this.arg2 = arg2
}
}
function wrapper(initial) {
// Rewrite initial constructor with our function
return function decoratedContructor(...args) {
// Create instance of initial object
const decorated = new initial(...args)
// Add some additional properties, methods
decorated.args = [...args]
// Return instantiated and modified object
return decorated
}
}
const decoratedPlugin = wrapper(Plugin)
const plugin = new decoratedPlugin('argument', { 'argument2': 1 })
console.log(plugin.args)
FYI: it's not safe to add properties without some prefix. Consider adding __ or something like this to your property, because you can accidentally rewrite some inner object property.
I was able to get this working with a modification to #guest271314's suggestion, namely, you need to pass ...initArgs to super(), otherwise webpack will fail with a TypeError: Cannot read property '...' of undefined.
Also took #terales's point into account about making sure to prefix my additional properties.
const exposeConstructorArgs = (Plugin, ...args) => {
const ExposedPlugin = class extends Plugin {
constructor(...initArgs) {
super(...initArgs);
this.__initArgs__ = initArgs;
}
get __initArgs() {
return this.__initArgs__;
}
};
return Reflect.construct(ExposedPlugin, args);
};
// ...
const dllPlugin = exposeConstructorArgs(webpack.DllPlugin, {
name: '[name]',
path: path.join(buildDir, '[name].json'),
});
// ...
const pluginConfig = dllPlugin.__initArgs[0];
expect(pluginConfig.name).toEqual('[name]');
You can use a generic function where class expression is used within function body. Pass reference to the class or constructor and parameters expected to be arguments within the instance to the function call.
function Plugin() {}
function Plugin2() {}
function PluginWrapper(pluginRef, ...args) {
let MyPlugin = class extends pluginRef {
constructor() {
super();
this.args = [...arguments];
}
getArgs() {
return this.args;
}
}
return Reflect.construct(MyPlugin, args);
};
const anInstance = PluginWrapper(Plugin, {
a: 'path'
});
console.log(anInstance.getArgs(), anInstance instanceof Plugin);
const aSecondInstance = PluginWrapper(Plugin2, "arg1", "arg2", {
b: 'anotherPath'
});
console.log(aSecondInstance.getArgs(), aSecondInstance instanceof Plugin2);

Avoiding the "default" property indirection with babel

I am using babel to transpile ES2015 code to ES5 & RequireJS.
But when I use the following syntax:
const o = { foo: 'foo' };
export default o;
The transpiled result is an object with a default property on it.
ie. it is currently transpiled to something like:
define(function() {
return {
default: {
foo: 'foo'
}
};
});
What I want is the object literal itself (containing the foo property) to be returned directly.
ie. I want something like:
define(function() {
return {
foo: 'foo'
};
});
Can I achieve this?
If you don't include default it will work as expected.
export const o = { foo: 'foo' };

Categories

Resources