Enumerations in Node.js and Mongoose
Enumerations are essential to writing good software. Enumerations provide a clear understanding of the set of values that can be used for a property. This ensures we can constrain input to the set of values in the enumeration. Enumerations also signal to the reader the intended behavior of model instances (particularly if the enumerated field is used in a state machine).
While JavaScript does not have a native implementation for an enumeration, it's easy to model one with a standard object. Combined with Mongoose, which does have a notion of enumerated properties, the pattern can be used to clean up domain code where the propagation of string/numeric literals is certain to cause errors or confusion.
For instance, let's imagine a Person
model:
const mongoose = require('mongoose');
const Genders = Object.freeze({
Male: 'male',
Female: 'female',
Other: 'other',
});
const PersonSchema = mongoose.Schema({
name: String,
gender: {
type: String,
enum: Object.values(Genders),
},
});
Object.assign(PersonSchema.statics, {
Genders,
});
module.exports = mongoose.model('person', PersonSchema);
When we want to make decisions about a Person's gender, we can use the exported enumeration:
const { Genders } = require('./models/person');
// Conditional logic becomes very clear and you are able to
// avoid errors where a literal might not match the intended enumerated value.
function conditionalExample(person) {
// This is bad because the enumerated value was 'female'. Also, consider what
// happens if the enumerated value changes? You would have to update
// many places in code.
if (person.gender === 'Female') {}
// This is better:
if (person.gender === Genders.Female) {}
}
// My favorite is using switch statements with enums:
function getSalutation(person) {
switch (person.gender) {
case Genders.Male: return 'Mr.';
case Genders.Female: return 'Mrs.';
default: return '';
}
}
Enumeration Style
There are many styles for enumerations, but I find the following conventions to be particularly helpful (especially if you follow the AirBnB JavaScript Style Guide):
- Use PascalCase when naming the enumeration and it's enumerated values.
- Pluralize the enumeration name.
- If the enumeration belongs to an entity, attach it as a static property.
- Freeze the enumeration object, preventing modifications to it.
If you follow these conventions, it will be very obvious when an enumeration is being used in code since variables will be camelCase, classes in singular form PascalCase, and constants in ALL_CAPS_SNAKE_CASE.
Protecting the Enumeration
If you decide to use enumerations, please make sure you make the object immutable. This is the purpose of Object.freeze
:
// rclayton@work:~$ node
const Genders = Object.freeze({
Male: 'male',
Female: 'female',
Other: 'other',
});
// undefined
Genders
// { Male: 'male', Female: 'female', Other: 'other' }
Genders.Male = 'female'
// 'female'
Genders
// { Male: 'male', Female: 'female', Other: 'other' }
ES6 Symbols
One question you may ask is why we don't use Symbol
instead of primitive values for an enum. The answer is a little tricky. It's highly unlikely your database or database client will utilize symbols cleanly. I imagine you could write a Mongoose plugin to do something like this, but I really just don't see the value in having to perform a bunch of pre-save and post-retrieval actions just to ensure you've got a no kidding Symbol
present on your model.
Symbols can actually be a pain sometimes as well. Take the following example from the Node.js cli:
// rclayton@work:~$ node
foo = Symbol('bar')
// Symbol(bar)
JSON.stringify({ foo });
// '{}'
JSON.stringify({ foo, hello: 'world' });
// '{"hello":"world"}'
foo
// Symbol(bar)
JSON.stringify({ foo2: foo, hello: 'world' });
// '{"hello":"world"}'
As you can see, symbols aren't serialized, as pointed out by the MDN documentation:
All Symbol-keyed properties will be completely ignored, even when using the replacer function.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
However, I do see the value of Symbol
in Node.js. It's an essential piece of metaprogramming and probably best used when writing frameworks where symbol evaluation is performed against objects that are not serialized.
References
AirBnB JavaScript Style Guide: https://github.com/airbnb/javascript
MSDN Description of PascalCase: https://msdn.microsoft.com/en-us/library/x2dbyw72(v=vs.71).aspx
MDN Object.freeze: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
MDN JSON.stringify: US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
You might also be interested in these articles...
Stumbling my way through the great wastelands of enterprise software development.