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

503 Views Asked by At

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!

3

There are 3 best solutions below

3
Ryan Wheale On

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
      }
    });
  }
}
0
Bergi On

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)

1
Aral Roca On

You can use defaultComposer to do this.

import { defaultComposer } from 'default-composer' // 300 B

const defaults = {
    type: 'myclass',
    prop1: {
        value1: 1,
        value2: 'two',
        value3: 'three'
    }
}

// Your class can mix them:
class SomeClass {
  constructor(args = {}) {
    this.data = defaultComposer(DEFAULTS, args);
  }
}

It works with nested values and also is configurable if you need a different logic to define these defaultables.