0

I have a CameraBuilder class that looks like this:

class CameraBuilder {
    constructor() {
        if (arguments.length) {
            throw new Error('[CameraBuilder constructor ERROR] class constructor does not accept parameters.');
        }
        this.camera = {};
    }

    withFarmLabel(farmLabel) {
        this.camera.farm_label = farmLabel;
        return this;
    }

    // more methods here

    build() {
        const missingProps = [];
        if (!this.camera.farm_label) {
            missingProps.push('\nMissing farm_label property. Use the withFarmLabel method in order to assign it.');
        }
        
        // more validations like the one above here

        if (missingProps.length) {
            const errorMsg = missingProps.join('');
            throw new Error(`[CameraBuilder build ERROR] ${errorMsg}`);
        }

        return this.camera;
    }
}

Since most of my validations are on the build() method and there are some business logic on some of these methods associated with how the user is building an instance of CameraBuilder, I wouldn't want anyone assigning cameraBuilderObj.camera directly. Is there any way I can enforce the use of the Class methods in order to assign properties to the Camera object?

Felipe Wagner
  • 154
  • 2
  • 14
  • 1
    The most straightforward way is to wait for ES10 and the proposed private fields using `#`. Only other real option is to use the functionality for private fields already implemented in TypeScript. – crashmstr Sep 22 '20 at 19:15
  • 1
    See also https://stackoverflow.com/a/52237988/1441 – crashmstr Sep 22 '20 at 19:16

2 Answers2

3

You could make the camera property private by putting # in front of it, ensuring that only CameraBuilder's internals can reference it:

class CameraBuilder {
    #camera = {};
    constructor() {
        if (arguments.length) {
            throw new Error('[CameraBuilder constructor ERROR] class constructor does not accept parameters.');
        }
    }

    withFarmLabel(farmLabel) {
        this.#camera.farm_label = farmLabel;
        return this;
    }

    // more methods here

    build() {
        const missingProps = [];
        if (!this.#camera.farm_label) {
            missingProps.push('\nMissing farm_label property. Use the withFarmLabel method in order to assign it.');
        }
        
        // more validations like the one above here

        if (missingProps.length) {
            const errorMsg = missingProps.join('');
            throw new Error(`[CameraBuilder build ERROR] ${errorMsg}`);
        }

        return this.#camera;
    }
}

const c = new CameraBuilder();
c.withFarmLabel('label');
console.log(c.camera);
console.log(c.build().farm_label);
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
1

CertainPerformance's answer probably makes more sense--don't expose it in the first place--but if for some reason you didn't want to go that route (or if you're in an environment where private fields aren't supported) you could define setters on it, so that direct assignments go through your function.

class Foo {
  constructor () {
    this._bar = 'baz';
  }

  set bar (value) {
    this._bar = value;
    console.log('do whatever you want to do here.');
  }
}

const f = new Foo();
f.bar = 'hey'; // direct assignment invokes the setter
ray
  • 26,557
  • 5
  • 28
  • 27
  • Since my setter sets values for properties for an object of the main class (CameraBuilder.camera), can I still take advantage of this? I mean, how this would stop the user from doing `cameraBuilderObj.camera.blabla = 'bleble'`? For what I understood, this only applies to the Class properties, right? – Felipe Wagner Sep 22 '20 at 19:22
  • You could do the same for any object. It's just syntactic sugar on top of [property descriptors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). You could just as easily create getters/setters for the camera object. – ray Sep 22 '20 at 20:18