FWS. S2E3. Generic smart setters.

Vasyl Boroviak
3 min readJul 21, 2021

--

Fun With Stamps. Season 2 — Best practices. Episode 3 — Generic smart setters.

Hello. I’m developer Vasyl Boroviak and welcome to the second season, third episode of Vasyl Boroviak presents Fun with Stamps.

In this season of FWS mini-articles we are going to learn stamp Best Practices using the stampit npm module.

TypeScript… TypeScript is great. It can protect you from assigning wrong type to a variable at compile-time. But TypeScript can’t protect you from assigning rubbish data at run-time. Stamps to the rescue.

I want some properties to be only strings, never else.

I’ll show you how to write a utility stamp (aka behaviour) which protect certain properties from being mistakenly set to some rubbish values.

I’d like to apply this logic before setting a value of a property:

if (value === undefined || typeof value === "string")

E.g.

myUser.fullName = "Roger Young";
// myUser.fullName === "Roger Young"
myUser.fullName = "Roger A. Young";
// myUser.fullName === "Roger A. Young"
myUser.fullName = [{rubbish: true}];
// myUser.fullName === "Roger A. Young"
myUser.fullName = undefined;
// myUser.fullName === undefined
myUser.fullName = {OtherRubbish: true};
// myUser.fullName === undefined

Also, I want this logic to be applied to only a few properties, like fullName, description, etc. Not all of them.

Here is how I want to use my utility behaviour (aka stamp):

const User = stampit(
HasStringAccessor.protectProperty("fullName", "description"),
{ name: "User" }, // ... maybe other stamps here?
);
const instance = User();
// instance.fullName === undefined
instance.fullName = function(){};
// instance.fullName === undefined
instance.fullName = "Roger Young";
// instance.fullName === "Roger Young"

Here is the code of the HasStringAccessor stamp:

const HasStringAccessor = stampit({
// stamp configuration
deepConf: {
// collect all the property names to protect
protectedProperties: []
},
statics: {
// API of the stamp
protectProperty(...names) {
// will concatenate the base array with `names`
return this.deepConf({ protectedProperties: names });
}
},
init(keyValueMap, { stamp }) {
// The properties to protect
const pp = stamp.compose.deepConfiguration.protectedProperties;
// The closured map of protected values
const map = new Map(pp.map(name => [name, undefined]));

for (const name of map.keys()) {
// Using JS getters and setters to protect properties
Object
.defineProperty(this, name, {
get () { return map.get(name); },
set (value) {
if (value === undefined || typeof value === "string")
map.set(name, value);
}
});
}

// Initialise this object instance with the configured values
for (const [key, value] of Object.entries(keyValueMap)) {
if (map.has(key)) this[key] = value;
}
}
});

We can go further and protect any property with any guardian logic. So, let’s implement this behaviour:

const _ = require("lodash");

const TestAccessors = stampit(
{ name: "TestAccessors" }, // ... maybe other stamps here?
HasProtectedAccessor.protectProperty({
fullName: v => v === undefined || _.isString(v),
address: _.isArray,
})
);

const instance1 = TestAccessors({
fullName: "John Johnson",
address: []
});
instance1.fullName = 123.123; // WILL NOT WORK! Still "John Johnson"
instance1.fullName = undefined; // ok

instance1.address = 123.123; // WILL NOT WORK! Still []
instance1.address = undefined; // WILL NOT WORK! Still []
instance1.address = ["123 Acme St"]; // ok

The HasProtectedAccessor utility stamp implementation:

const HasProtectedAccessor = stampit({
// stamp configuration
deepConf: {
// collect all the property names to protect
protectedProperties: {}
},
statics: {
// API of the stamp
protectProperty(names) {
// will deep merge the base objects with `names`
return this.deepConf({ protectedProperties: names });
}
},
init(keyValueMap, { stamp }) {
// The properties to protect
let { protectedProperties } = stamp.compose.deepConfiguration || {};
// The closured map of protector functions
const protectors = new Map(Object.entries(protectedProperties)
.filter(([key, value]) => typeof value === "function"));
// The closured map of protected values
const map = new Map();
for (const key of protectors.keys()) map.set(key, undefined);

for (const name of map.keys()) {
// Using JS getters and setters to protect properties
Object
.defineProperty(this, name, {
get () { return map.get(name); },
set (value) {
if (protectors.get(name)(value, name))
map.set(name, value);
}
});
}

// Initialise this object instance with the configured values
for (const [key, value] of Object.entries(keyValueMap)) {
if (map.has(key)) this[key] = value;
}
}
});

Have fun with stamps!

The rest of the episodes:

--

--