As I had a problem where my constructor became quite large and it started to become a problem maintaining it. I created this solution as a start of the refactor, however this still looked bulky. And it was easy to make a mistake.
constructor(data: Partial<BusinessConfiguration>) {
if(!data) return;
this.logoId = data.logoId;
this.positiveLogoId = data.positiveLogoId;
this.negativeLogoId = data.negativeLogoId;
this.faviconId = data.faviconId;
this.primaryColor = data.primaryColor;
this.windowTabName = data.windowTabName;
this.shouldShowGDPR = data.shouldShowGDPR;
this.allowsTermination = data.allowsTermination;
this.styleConfiguration = data.styleConfiguration;
this.shouldUseCustomTitle = data.shouldUseCustomTitle;
this.processNotesStickers = data.processNotesStickers;
this.terminationProcesses = data.terminationProcesses;
this.shouldUseCustomHeader =data.shouldUseCustomHeader;
this.completeDelete = data.completeDelete
this.pingConfiguration = data.pingConfiguration;
this.passwordConfiguration = data.passwordConfiguration;
this.shouldAllowRandomRepeat = data.shouldAllowRandomRepeat;
this.shouldRedirectImmediately = data.shouldRedirectImmediately;
}
So I created this, with this logic and good validation in my routes I never have to updated my constructor again. Whatever we send, those properties will be assigned. However, I am a bit unhappy with the fact that I am kind of brute forcing this. I want to check if the element from data, exists as a property of the class BusinessConfiguration.
constructor(data: Partial<BusinessConfiguration>) {
if(!data) return;
Object.keys(data).forEach(element => {
this[element] = data[element]
});
}
I had this problem on multiple occasions and maybe this is not the best example as I do have a type on data. But if data enters this constructor as type of any, mistakes and unwanted things may happen.
So bottom line is, How to access class property keys before object initialization.
I had an idea to create some kind of data structure which would hold all class property keys, but I hope there is something more clean. And if there is not, how would I go about making that as clean as possible, ideas are welcomed. Thank you in advance :)
Rather than defining fields in the constructor, you can use public class fields:
class Business {
// Defines a property but without a value (undefined).
// You can add TypeScript types to these.
logoId;
positiveLogoId;
negativeLogoId;
// ...
}
Next, in the constructor you can include a loop that's similar to what you have, but with an extra filter:
class Business {
logoId;
positiveLogoId;
negativeLogoId;
constructor(data) {
Object
.keys(data)
.filter(key => this.hasOwnProperty(key))
.forEach((element) => {
this[element] = data[element];
});
}
}
A different syntax, using the modern hasOwn and for...of:
class Business {
logoId;
positiveLogoId;
negativeLogoId;
constructor(data) {
for (const [key, value] of Object.entries(data)) {
if (Object.hasOwn(this, key)) {
this[key] = value;
}
}
}
}
Using either of the above, you can now only set properties that exist:
class Business {
logoId;
positiveLogoId;
negativeLogoId;
constructor(data) {
for (const [key, value] of Object.entries(data)) {
if (Object.hasOwn(this, key)) {
this[key] = value;
}
}
}
}
const b = new Business({
logoId: 5,
positiveLogoId: 10,
foo: 1,
});
console.log(
'Does the object contain "logoId"?',
Object.keys(b).includes('logoId'),
'(should be true)'
);
console.log(
'Does the object contain "foo"?',
Object.keys(b).includes('foo'),
'(should be false)'
);
The best answer I could come up with is to have a static variable on the class where I would store property names, but then this array needs to be updated, which is not much different than updating the constructor, but I do get an added benefit where I can access the keys of this class for this kind of operations if I need.
class Business {
private static keys = ['logoId', 'positiveLogoId', 'negativeLogoId']
logoId;
positiveLogoId;
negativeLogoId;
constructor(data: Partial<Business>) {
Object.keys(data).forEach((element) => {
if (Business.keys.includes(element))
this[element] = data[element];
});
}
}
const obj = new Business({
logoId: 5,
positiveLogoId: '10',
foo: 1
}as any)
console.log(obj) // Business { logoId: 5, positiveLogoId: '10'}
You could introduce a helper that takes an object and an array of property names. Then returns a new object that only contains the given properties (if present).
function objSlice(obj, props = []) {
return Object.fromEntries(
props
.filter(prop => prop in obj)
.map(prop => [prop, obj[prop]])
);
}
Example usage:
const data = { a: 1, b: 2, c: 3 };
objSlice(data, ["c", "a", "d"]);
//=> { c: 3, a: 1 }
You can use this in combination with Object.assign() and a static propNames attribute that holds allowed property names to very easily filter out undesired properties.
Object.assign(this, objSlice(data, this.constructor.propNames));
Here is an example:
class Book {
static propNames = ["title", "author", "releaseYear"];
constructor(data = {}) {
Object.assign(this, objSlice(data, this.constructor.propNames));
}
toString() {
return `${this.title} (${this.releaseYear}) - ${this.author}`;
}
}
const helloWorld = new Book({
title: "Hello World!",
author: "John Doe",
releaseYear: 1337,
// undesired data property
toString() {
return "conflicting with Book method";
},
});
console.log(`The book "${helloWorld}" was a good read.`);
// helper
function objSlice(obj, props = []) {
return Object.fromEntries(
props
.filter(prop => prop in obj)
.map(prop => [prop, obj[prop]])
);
}