HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

Attempt at type-safe enums in JavaScript

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
javascriptattempttypesafeenums

Problem

I wrote this tiny library yesterday. The goal was to implement enums in JavaScript with type-safety. I modeled the implementation similar to enums in Java, since that is what I am most familiar with.

It seems to work like you would expect, but I wanted to make sure that what I did makes sense and that there isn't some glaring hole in my implementation, and that enum semantics are appropriately conveyed. Suggestions for improvement are appreciated also.

```
/**
* enum.js - Type-safe enums in JavaScript. Modeled after Java enums.
* Version 1.0.0
* Written by Vivin Paliath (http://vivin.net)
* License: BSD License
* Copyright (C) 2015
*/

var Enum = (function () {
/**
* Function to define an enum
* @param typeName - The name of the enum.
* @param constants - The constants on the enum. Can be an array of strings, or an object where each key is an enum
* constant, and the values are objects that describe attributes that can be attached to the associated constant.
*/
function define(typeName, constants) {

/ Check Arguments /
if (typeof typeName === "undefined") {
throw new TypeError("A name is required.");
}

if (!(constants instanceof Array) && (Object.getPrototypeOf(constants) !== Object.prototype)) {

throw new TypeError("The constants parameter must either be an array or an object.");

} else if ((constants instanceof Array) && constants.length === 0) {

throw new TypeError("Need to provide at least one constant.");

} else if ((constants instanceof Array) && !constants.reduce(function (isString, element) {
return isString && (typeof element === "string");
}, true)) {

throw new TypeError("One or more elements in the constant array is not a string.");

} else if (Object.getPrototypeOf(constants) === Object.prototype && !Object.keys(constants).reduce(function (isObject, constant) {

Solution

Why use getters like name() and ordinal() instead of properties? In Java
you might use these even when they do nothing more than return a private
property so as to future-proof your code: if, one day, you want to add some
logic to count the number of times an Enum name is fetched, or otherwise do
more than only return a value, then you can do this without having to modify
every bit of source that had referenced a property and must now call a
function. But there's no need for this kind of future-proofing in JavaScript:

var a = { get x() { return "got x" } }
a.x  // 'got x'


Why should Days.Friday.fromName('Saturday') work? Is this a real, intended
feature, or just something that fell out of your code?

Why freeze everything? Object.freeze() calls are easy to add to the
following code, I just wonder what the point is.

Do enums have a name for any reason other than to show up in their string representation? Again, easy to add back to the following if you really want it.

Mainly, this strikes me as an incredible amount of work for what it does.

var Enum = {
  define: function() {
    var type = function() {},
        length = arguments.length,
        enums = [],
        string = "Enum { "

    type.prototype.valueOf = type.prototype.toString = function() { return this.name }
    type.fromName = function(name) { return type[name] }
    type.values = function() { return enums }
    type.toString = function() { return string }

    for (var i = 0; i  0) string += ", "
      string += o.name
    }
    string += " }"

    return type
  }
}


Usage is retained, with one exception:

var Days = Enum.define("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")

console.log(true, Days.Monday instanceof Days)
console.log('Friday', Days.Friday.name)
console.log(4, Days.Friday.ordinal)
console.log(true, Days.Sunday === Days.Sunday)
console.log(false, Days.Sunday === Days.Friday)
console.log('Sunday', Days.Sunday.toString())
console.log('Enum { Monday, Tuesday, ... }', Days.toString())
console.log('["Monday", ...]', Days.values().map(function(e) { return e.name }))
console.log('Friday', Days.values()[4].name)
console.log(true, Days.fromName("Thursday") === Days.Thursday)
console.log('Wednesday', Days.fromName("Wednesday").name)
// console.log('Saturday', Days.Friday.fromName('Saturday').name)


EDIT: I didn't even notice the protection against new instances of the Enum constructor. This seems to me to be an unlikely error... but this kind of protection is easily added with a check against a closed-over lexical variable:

var Terminable = (function() {
  var terminated = false,
      constructor = function() {
        if (terminated)
          throw new TypeError("Cannot instantiate a new instance of a terminated constructor")
      }
  constructor.terminate = function() { terminated = true }
  return constructor
})()

var a = new Terminable
console.log(true, a instanceof Terminable)  // true true
Terminable.terminate()
var b = new Terminable  // TypeError

Code Snippets

var a = { get x() { return "got x" } }
a.x  // 'got x'
var Enum = {
  define: function() {
    var type = function() {},
        length = arguments.length,
        enums = [],
        string = "Enum { "

    type.prototype.valueOf = type.prototype.toString = function() { return this.name }
    type.fromName = function(name) { return type[name] }
    type.values = function() { return enums }
    type.toString = function() { return string }

    for (var i = 0; i < length; i++) {
      var o = new type
      type[arguments[i]] = o
      o.name = arguments[i]
      o.ordinal = i
      enums.push(o)

      if (i > 0) string += ", "
      string += o.name
    }
    string += " }"

    return type
  }
}
var Days = Enum.define("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")

console.log(true, Days.Monday instanceof Days)
console.log('Friday', Days.Friday.name)
console.log(4, Days.Friday.ordinal)
console.log(true, Days.Sunday === Days.Sunday)
console.log(false, Days.Sunday === Days.Friday)
console.log('Sunday', Days.Sunday.toString())
console.log('Enum { Monday, Tuesday, ... }', Days.toString())
console.log('["Monday", ...]', Days.values().map(function(e) { return e.name }))
console.log('Friday', Days.values()[4].name)
console.log(true, Days.fromName("Thursday") === Days.Thursday)
console.log('Wednesday', Days.fromName("Wednesday").name)
// console.log('Saturday', Days.Friday.fromName('Saturday').name)
var Terminable = (function() {
  var terminated = false,
      constructor = function() {
        if (terminated)
          throw new TypeError("Cannot instantiate a new instance of a terminated constructor")
      }
  constructor.terminate = function() { terminated = true }
  return constructor
})()

var a = new Terminable
console.log(true, a instanceof Terminable)  // true true
Terminable.terminate()
var b = new Terminable  // TypeError

Context

StackExchange Code Review Q#105032, answer score: 3

Revisions (0)

No revisions yet.