JS Tip of the Day: Define vs. Assign

Define vs. Assign
Level: Advanced

There are two ways to set a value for an object property. The first, and most common, is through assignment. Assignment is what happens with normal usage of the assignment operator (=). For data properties, this sets the value of that property, and for accessor properties, it runs the setter function, passing the value getting assigned into it.

You can also define a property. Defining happens when you use Object.defineProperty(). Defining differs from assigning in that a define does not run setter functions on accessor properties. A define would instead fully replace the accessor with the new property definition.

let reportCard = {
    get grade () {
        return 'F';
    },
    set grade (value) {
        console.log('Not gonna happen');
    }
};

console.log(reportCard.grade); // F

// assign
reportCard.grade = 'A+'; // Not gonna happen
console.log(reportCard.grade); // F

// define
Object.defineProperty(reportCard, 'grade', { value: 'A+' });
console.log(reportCard.grade); // A+

Different syntaxes and APIs use different approaches to setting properties.

  • Assigns:

    • Assignment operator (=)
    • Object.assign()
    • Reflect.set()
  • Defines:

    • Object.defineProperty()/Object.defineProperties()
    • Object.create()
    • Object.fromEntries()
    • Spread (...)
    • Object literal ({}) and class members

The last define in that list is one worth exploring further, especially in terms of class and class fields (a stage 3 proposal). The way class fields are initialized in the class syntax, looks much like an assign since it uses a equals sign to apply the initial value. But instead of using assign, class fields use define. This is contrary to how the property would have been set if through the constructor despite looking very similar.

class MyClass {
    prop = 1; // define
    constructor () {
        this.prop = 1; // assign
    }
}

This can be an important distinction to make, especially if the property is an inherited accessor property. That being the case, a class field would override the accessor in the instance with a data property whereas setting the property in the constructor would run the accessor setter.

As an example, we can extend Set and try setting its size property. The size property in set objects is a getter with no setter. If you attempt to set it normally, you’ll get an error (in strict mode). But if you use a class field, it will define a new size data property that overrides the original.

class UnsizableSet extends Set {
    constructor () {
        super();
        this.size = 3;
    }
}
let unsizable = new UnsizableSet(); // TypeError
// (trying to assign existing setter)

class SizableSet extends Set {
    size = 3;
}
let sizable = new SizableSet(); // Ok
console.log(sizable.size); // 3

Now, despite the name, SizableSet does not make an actual sizable set. The size property being defined is separate from the one representing the size of the set and does not reflect, nor is able to change, its true size.

That said, it’s important to point out that class fields are still in the proposal stages of development and the define behavior described here could potentially change before finalized - in fact this behavior in particular is one of the topics still under contention for this feature. TypeScript will also use assign for class fields by default unless you enable the --useDefineForClassField flag.

More info: