gschenk
gschenk

Reputation: 1057

Object constructor from template object

For sports, I try to build a constructor to construct an object based on a template.

Let template and data two objects where the keys of data are a subset of the keys of template:

const template = { a: 'foo', b: 'bar' };
const data = { a: 42 };

These object literals are examples. The question is how to do it for an arbitrary object of same structure. That is, properties (keys) are not know when coding the class constructor.

I should like to write a constructor with which I can construct an Object based on this template with the following expression:

foo = new Foo (template, data)

where console.log(foo) returns for above example // foo = { a: 42 }

That means, the keys present in the Object are to be defined by the template object literal.

So far, I have not found a good way to do so in ES6 (or newer) JS.

Q: How might such a constructor look like? What are good patterns for a class based approach to handle data in such objects?

A hackish way is to have Object.assign(this, data) in the constructor. For example:

class Foo {
  constructor(template, data) {
    localObject = niftyMergeFunction(template, data);
    Object.assign(this, localObject);
  }
}

However, this is bad for several reasons. It is a statement and not an expression. It is not type-safe.

I should like to avoid inheritance, for (i) I avoid learning outdated and controversial practises it and prefer composition. What is more, (ii) template can have a common ancestor with data but cannot be its parent for conceptual reasons. (consider example: human is animal, cow is animal; don't inherit animal from human and cow from animal, and perhaps another instance of human from animal as well.)

No prototype based designs either, since it appears to be somewhat out of favour at present.

Please note, at this point the task is trivially solved by foo = Object.assign({}, data). However, later i shall use partial assignment cont Bar = in => Foo(template, in) and send different data structures instead of object data, such as bar = new Bar(someFunction(data)); How to curry/partially assign to constructors would be a different question though, please consider this as merely context now.

Upvotes: 0

Views: 1535

Answers (3)

gschenk
gschenk

Reputation: 1057

It is possible and straight forward after all to have properties in a constructor in class syntax based on arguments passed to the constructor. Mapping a function along the lines of a => this[a] onto an array of the property keys does it. In the snippet is an example where there is also an assignment to the property.

// example objects
const someObject = {
  a: 0,
  b: 0,
  c: 0,
};

const otherObject = {
  a: 1,
  c: 137,
  d: 42,
};

class Test {
  constructor(template, data) {
    Object.keys(template).map(a => {
      this[a] = data[a] ? data[a] : template[a];
      return null;
    });
    Object.seal(this);
  }
}

const test = new Test(someObject, otherObject);
console.log(test);
As a nice addition its new objecst already has an Object.seal applied during construction.

further remarks

  • If one cannot use Object.seal yet, inserting Object.freeze at the end of the property generating function will help with a deep freeze.

  • I cannot get my actual purpose for this working: that is partial application of the template object. The idea was to have new monovariate class constructor functions. fixed

Upvotes: 1

Ben Aston
Ben Aston

Reputation: 55739

JavaScript is an object-based language. Classes in JavaScript are mostly syntactic sugar. If, like me, you don't like class-oriented object orientation, then you do not have to use it.

If you want to write a function to combine objects with arbitrary properties, there is nothing "hackish" with using Object.assign.

If you want to prevent further modification of the result, you can use Object.freeze.

I think the first question you need to ask yourself is "do I really need to do this?" Are you trying to make JavaScript look and work like Haskell? Is it worth it? It is possible, however, you have a good reason for doing what you want to do.

The following code might be of use.

const curry = (fn, acc = [], cont) =>
      cont = (...args) => 
        (acc = [...acc, ...args], 
          args.length ? cont : fn(...acc))

const mapObject = (mapProperty, to, from) => 
    (Object.keys(to).forEach(k => 
        (isObject(to[k]) && isObject(from[k])) 
            ? mapObject(mapProperty, to[k], from[k])
            : mapProperty(to, from, k)), to)

const isObject = (o) => o !== null && typeof o === 'object'
const deepClone = (o) => JSON.parse(JSON.stringify(o))
const mapProperty = (to, from, key) => (from.hasOwnProperty(key) && (to[key] = from[key]))
const deepAssign = mapObject.bind(null, mapProperty)

const factory = curry((template, ...data) => 
                    data.reduce((acc, datum) => 
                        deepAssign(acc, datum), deepClone(template)))

const template = { a: '', b: '', c: { d: '' }, e: '', f: '' }
const myFactory = factory(template)
const o1 = { a: 'a'}
const o2 = { b: 'b' }
const o3 = { c: { d: 'd' } }
const o4 = { f: 'f' }
const o5 = { g: 'g' }

console.log(myFactory(o1)(o2)(o3)(o4)(o5)())

Upvotes: 1

Sydney Y
Sydney Y

Reputation: 3152

class Foo {
  constructor (data, template) {
    this.data = data
    this.template = template
  }
  setTemplate (template) {
    this.template = template
  }
  get value () {
    let output = {}
    output[this.data] = this.template
    return output
  }
}

Edited to reflect feedback in comment

Upvotes: 1

Related Questions