Node.js: Creating a class with properties in JavaScript

Defining properties as unchangeable can be achieved by correctly assigning types to the constructor and utilizing the utility type [ ]. For increased future proofing, accepting the entire parameter list for the constructor is recommended. Other properties should be collected separately to ensure API stability. A convenience method has been added that also provides cloning functionality. However, it is important to note that the code snippet includes an asynchronous function running inside a synchronous function, which can be solved by using arrow functions to provide lexical this in function scope.


Solution 1:

By utilizing destructuring with the spread syntax, you can extract all the additional information from the object passed to the Document constructor beyond the basic details.

For this demonstration, I have disabled the TypeScript segments in order to allow it to function properly.

class Document {
    basicDetails /*: BasicDetails */;
    constructor(objDocument /*:any*/) {
        let {basicDetails, ...rest} = objDocument;
        this.basicDetails = new BasicDetails(basicDetails);
        Object.assign(this, rest);
    }
}
class BasicDetails {
    type /*: string */;
    createdDate /*: Date */;
    constructor(objDocument /*:any */) {
        this.type = objDocument.type;
        this.createdDate = new Date();
    }
}
let obj = {basicDetails: {type: "draft"}, lineDetails:{lineid:123,itemName:"itemName"} };
let doc = new Document(obj);
console.log(doc);


Solution 2:


@trincot’s response has been enhanced. To begin with, the design could be improved by injecting an existing class into the Document constructor instead of the current approach.

class Document {
    basicDetails /*: BasicDetails */;
    constructor(basicDetails /*:BasicDetails*/, rest : /*:any*/) {
       this.basicDetails = basicDetails
       Object.assign(this, rest);
    }
}

The SOLID principle known as Dependency Injection is among the set of principles.

let obj = { basicDetails: { type: "draft" }, lineDetails:{ lined : 123, itemName : "itemName" } };
class BasicDetails {
    type /*: string */;
    createdDate /*: Date */;
    constructor(basicDetails /*:basicDetails */) {
        this.type = basicDetails.type;
        this.createdDate = new Date();
    }
}
const { basicDetails, ...rest } = obj
const doc = new Document( new BasicDetails(basicDetails), rest )


Solution 3:


To simplify your life, start by retrieving the constructor choices for

Document

. The objective is to have a

basicDetails

at the primary level, followed by anything else that may come up.

interface DocumentOptions {
    basicDetails: any,
    [key: string]: unknown
}

It is now possible to achieve a level of type safety and prohibit the creation of

Document

if

basicDetails

is not present.

Modify the constructor to retrieve

basicDetails

while creating the

BasicDetails

instance. The remaining properties can be assigned to the current instance. By including an [index signature] for

Document

, it can have an indefinite number of properties. However, if you make these properties

readonly

, you won’t be able to modify them.

class Document {
    basicDetails: BasicDetails;
    readonly [key: string]: unknown
    constructor({ basicDetails, ...otherDetails } : DocumentOptions) {
        this.basicDetails = new BasicDetails(basicDetails);
        Object.assign(this, otherDetails);
    }
}

To enhance type safety, it is advisable to assign accurate types for

BasicDetails

constructor and utilize the

ConstructorParameters

utility type to ensure the proper parameters are passed to

Document

.

interface DocumentOptions {
    basicDetails: ConstructorParameters[0],
    [key: string]: unknown
}
class Document {
    basicDetails: BasicDetails;
    readonly [key: string]: unknown
    constructor({ basicDetails, ...otherDetails } : DocumentOptions) {
        this.basicDetails = new BasicDetails(basicDetails);
        Object.assign(this, otherDetails);
    }
}
class BasicDetails {
    type: string;
    createdDate: Date;
    constructor(objDocument: {}) {
        this.type = "";
        this.createdDate = new Date();
    }
}
let obj = { basicDetails: { type: "draft" }, lineDetails: { lineid: 123, itemName: "itemName" } }
let doc = new Document(obj)
doc["lineDetails"] = obj.lineDetials // not acceptable
console.log(doc)

To ensure maximum future proofing, consider accepting the complete parameter list for

BasicDetails

. By doing so, you won’t have to make any modifications to

Document

or

DocumentOptions

if the constructor is altered in the future.

interface DocumentOptions {
    basicDetails: ConstructorParameters, //no index
    [key: string]: unknown
}
class Document {
    basicDetails: BasicDetails;
    readonly [key: string]: unknown
    constructor({ basicDetails, ...otherDetails } : DocumentOptions) {
        this.basicDetails = new BasicDetails(...basicDetails);
//pass everything                            ^^^
        Object.assign(this, otherDetails);
    }
}

Nevertheless, I would suggest gathering all remaining attributes and placing them in an exclusive property. This approach would enhance the dependability of the

Document

API.

interface DocumentOptions {
    basicDetails: ConstructorParameters[0],
    [key: string]: unknown
}
class Document {
    basicDetails: BasicDetails;
    readonly otherDetails: Readonly>
    constructor({ basicDetails, ...otherDetails }: DocumentOptions) {
        this.basicDetails = new BasicDetails(basicDetails);
        this.otherDetails = otherDetails;
    }
    serialise() {
        return Object.assign({}, {basicDetails: this.basicDetails.serialise()}, this.otherDetails)
    }
}
class BasicDetails {
    type: string;
    createdDate: Date;
    constructor(objDocument: {}) {
        this.type = "";
        this.createdDate = new Date();
    }
    serialise() {
        return { type: this.type };
    }
}
let obj = { basicDetails: { type: "draft" }, lineDetails: { lineid: 123, itemName: "itemName" } }
let doc = new Document(obj)
doc.otherDetails["lineDetails"] = obj.lineDetails // not acceptable
console.log(doc.serialise())

For your convenience, I have included a method

serialise

that will produce the expected information output.

{
  "basicDetails": {
    "type": "",
    "createdDate": "2020-12-03T09:09:30.355Z"
  },
  "lineDetails": {
    "lineid": 123,
    "itemName": "itemName"
  }
} 

In addition, it provides cloning capability without any extra effort, as it produces the same data that was used to construct it, allowing for the following action to be taken:

let obj = { basicDetails: { type: "draft" }, lineDetails: { lineid: 123, itemName: "itemName" } }
let doc1 = new Document(obj)
//free cloning
let doc2 = new Document(doc1.serialise());


Frequently Asked Questions