FWS. S2E3. Generic smart setters.
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 === undefinedmyUser.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 === undefinedinstance.fullName = function(){};
// instance.fullName === undefinedinstance.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:
- Season one
- S2E1. Naming stamps
- S2E2. Property order
- S2E3. Generic smart setters (this article)