0

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!

  • You should just make your own recursive merger function – kelsny Jan 13 '23 at 19:32
  • 3
    The fact that a field ( like prop1) could hold either an object or a string or maybe something else seems to maybe not be the right design. I think you could simplify your case by having consistent types for each field. – Peterrabbit Jan 13 '23 at 19:35
  • @Peterrabbit I'm sorry, I might not have been specific enough. The whole idea is to have a parent setting (let's say `willChange = true` (always)) which might be fine-tuned by adding additional configuration options (`willChange: { onOccasion1: true, onOccasion2: false, onOccasion3: true }` – Valentin Alisch Jan 13 '23 at 20:28
  • If that's what you want and if I were you, I'd take a value, or a function that returns the value. What needs figuring out here is which parameters, if any, the function needs to perform the operation. – José Ramírez Jan 13 '23 at 20:32

3 Answers3

0

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)

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
0

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.

Aral Roca
  • 5,442
  • 8
  • 47
  • 78
  • 1
    This is an ok answer, but please [disclose that this is your own library](https://stackoverflow.com/help/promotion) – Bergi Jun 06 '23 at 11:56
-1

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
      }
    });
  }
}
Ryan Wheale
  • 26,022
  • 8
  • 76
  • 96
  • 1
    I think it would be reasonable to store default values inside the class. For example in static private field – Jaood_xD Jan 13 '23 at 20:17
  • Yeah I thought so too. Could you please clarify why you're thinking that »[...] It's very strange and smelly...«. Thank you :) – Valentin Alisch Jan 13 '23 at 20:22
  • You can store defaults anywhere you like. I see no value in polluting the class with extra properties... but do what you will. The "strange and smelly" is strange because I have never seen anything like that before, and smelly because it's a "code smell" - programmer lingo for _"weirdly written and a likely source of bugs"_. – Ryan Wheale Jan 13 '23 at 20:25