How to implement Swift-like enums with associated values in JavaScript?
You can use a discriminated union for this. This example uses Typescript, but the concept would be similar for Javascript just without the type safety.
interface UPCA { kind: "UPCA"; numberSystem: number; manufacturer: number; item: number; checkDigit: number;}interface QRCode { kind: "QRCode"; data: string;}interface Other { kind: "Other";}type Barcode = UPCA | QRCode | Other;
You can then switch over a value of Barcode
and access the associated values after checking the discriminate.
function printBarcode(barcode: Barcode) { switch (barcode.kind) { case "UPCA": console.log(`UPCA: ${barcode.numberSystem}-${barcode.manufacturer}`); break; case "QRCode": console.log(`QRCode: ${barcode.data}`); break; case "Other": console.log("Other barcode"); break; } }
That's notr exactly the way enums work in most languages i know. Usually they are more like a way to type a value as one of these states. Like selecting one value out of a set of possible values. And to ensure type-safety in doing this, unlike with plain integers.
What you posted in your code, I would call a plain Object with factory-methods.
Since they are not supported that way by the language you have to implement them in a way that fit's your needs as good as possible. So sum up what behaviour you expect.
In the mean time a Implementation based on the descriptions i've found on swift enums. hope it comes close to what you expect:
var odp = { ENUMERABLE: 4, //two helper with Object.defineProperty. value: function(obj, prop, v, flags){ this.configurable = Boolean(flags & odp.CONFIGURABLE); this.writable = Boolean(flags & odp.WRITABLE); this.enumerable = Boolean(flags & odp.ENUMERABLE); this.value = v; Object.defineProperty(obj, prop, this); this.value = null; //v may be a function or an object: remove the reference return obj; }.bind({ //caching the basic definition value: null, configurable: false, writable: false, enumerable: false }), accessor: function(obj, prop, getter, setter){ this.get = getter || undefined; this.set = setter || undefined; Object.defineProperty(obj, prop, this); this.get = null; this.set = null; return obj; }.bind({ get: null, set: null })}//make these values immutableodp.value(odp, "CONFIGURABLE", 1, odp.ENUMERABLE);odp.value(odp, "WRITABLE", 2, odp.ENUMERABLE);odp.value(odp, "ENUMERABLE", 4, odp.ENUMERABLE);//Policy: //1. I don't f*** care wether the keys on the definition are own or inherited keys.//since you pass them to me, I suppose you want me to process them.//2. If i find some undefined-value i ignore it, as if it wasn't there.//use null to represent some "empty" value//name and extendProto are optionalfunction Enum(name, description, extendProto){ var n = name, d = description, xp=extendProto; if(n && typeof n === "object") xp=d, d = n, n = null; var xpf = typeof xp === "function" && xp; var xpo = typeof xp === "object" && xp; function type(){ throw new Error("enums are not supposed to be created manually"); } //abusing filter() as forEach() //removing the keys that are undefined in the same step. var keys = Object.keys(d).filter(function(key){ var val = d[key]; if(val === undefined) return false; var proto = Object.create(type.prototype); //your chance to extend the particular prototype with further properties //like adding the prototype-methods of some other type var props = xpf || xpo && xpo[key]; if(typeof props === "function") props = props.call(type, proto, key, val); if(props && typeof props === "object" && props !== proto && props !== val){ var flags = odp.CONFIGURABLE+odp.WRITABLE; for(var k in props) props[k]===undefined || odp.value(proto, k, props[k], flags); if("length" in props) odp.value(props, "length", props.length, flags); } if(typeof val === "function"){ //a factory and typedefinition at the same type //call this function to create a new object of the type of this enum //and of the type of this function at the same time type[key] = function(){ var me = Object.create(proto); var props = val.apply(me, arguments); if(props && typeof props === "object" && props !== me){ for(var k in props) props[k]===undefined || odp.value(me, k, props[k], odp.ENUMERABLE); if("length" in props) odp.value(me, "length", props.length); } return me; } //fix the fn.length-property for this factory odp.value(type[key], "length", val.length, odp.CONFIGURABLE); //change the name of this factory odp.value(type[key], "name", (n||"enum")+"{ "+key+" }" || key, odp.CONFIGURABLE); type[key].prototype = proto; odp.value(proto, "constructor", type[key], odp.CONFIGURABLE); }else if(val && typeof val === "object"){ for(var k in val) val[k] === undefined || odp.value(proto, k, val[k]); if("length" in val) odp.value(proto, "length", val.length); type[key] = proto; }else{ //an object of the type of this enum that wraps the primitive //a bit like the String or Number or Boolean Classes //so remember, when dealing with this kind of values, //you don't deal with actual primitives odp.value(proto, "valueOf", function(){ return val; }); type[key] = proto; } return true; }); odp.value(type, "name", n || "enum[ " + keys.join(", ") + " ]", odp.CONFIGURABLE); Object.freeze(type); return type;}
Beware, this code may need some further modification. Examples:
Factories
function uint(v){ return v>>>0 }var Barcode = Enum("Barcode", { QRCode: function(string){ //this refers to an object of both types, Barcode and Barcode.QRCode //aou can modify it as you wish odp.value(this, "valueOf", function(){ return string }, true); }, UPCA: function(a,b,c,d){ //you can also return an object with the properties you want to add //and Arrays, ... return [ uint(a), uint(b), uint(c), uint(d) ]; //but beware, this doesn't add the Array.prototype-methods!!! //event this would work, and be processed like an Array return arguments; }, Other: function(properties){ return properties; //some sugar }});var productBarcode = Barcode.UPCA(8, 85909, 51226, 3);console.log("productBarcode is Barcode:", productBarcode instanceof Barcode); //trueconsole.log("productBarcode is Barcode.UPCA:", productBarcode instanceof Barcode.UPCA); //trueconsole.log("productBarcode is Barcode.Other:", productBarcode instanceof Barcode.Other); //falseconsole.log("accessing values: ", productBarcode[0], productBarcode[1], productBarcode[2], productBarcode[3], productBarcode.length);Array.prototype.forEach.call(productBarcode, function(value, index){ console.log("index:", index, " value:", value);});
Objects and Primitives
var indices = Enum({ lo: { from: 0, to: 13 }, hi: { from: 14, to: 42 }, avg: 7});var lo = indices.lo;console.log("lo is a valid index", lo instanceof indices);console.log("lo is indices.lo", lo === indices.lo); //indices.lo always references the same Object//no function-call, no getter!var avg = indices.avg; //beware, this is no primitive, it is wrappedconsole.log("avg is a valid index", avg instanceof indices);console.log("comparison against primitives:");console.log(" - typesafe", avg === 7); //false, since avg is wrapped!!!console.log(" - loose", avg == 7); //trueconsole.log(" - typecast+typesafe", Number(avg) === 7); //true//possible usage like it was a primitive.for(var i=lo.from; i<lo.to; ++i){ console.log(i, i == avg); //take a look at the first output ;)}//but if you want to use some of the prototype methods //(like the correct toString()-method on Numbers, or substr on Strings)//make sure that you have a proper primitive!var out = avg.toFixed(3);//will fail since this object doesn't provide the prototype-methods of Number//+avg does the same as Number(avg)var out = (+avg).toFixed(3); //will succeed
Identity
var def = { foo: 42 };var obj = Enum({ a: 13, b: 13, c: 13, obj1: def, obj2: def});//although all three have/represent the same value, they ain't the samevar v = obj.a;console.log("testing a", v === obj.a, v === obj.b, v===obj.c); //true, false, falsevar v = obj.b;console.log("testing a", v === obj.a, v === obj.b, v===obj.c); //false, true, falsevar v = obj.c;console.log("testing a", v === obj.a, v === obj.b, v===obj.c); //false, false, trueconsole.log("comparing objects", obj.obj1 === obj.obj2); //falseconsole.log("comparing property foo", obj.obj1.foo === obj.obj2.foo); //true//same for the values provided by the factory-functions:console.log("compare two calls with the same args:");console.log("Barcode.Other() === Barcode.Other()", Barcode.Other() === Barcode.Other());//will fail, since the factory doesn't cache, //every call creates a new Object instance.//if you need to check wether they are equal, write a function that does that.
extendProto
//your chance to extend the prototype of each subordinated entry in the enum//maybe you want to add some method from some other prototype //like String.prototype or iterator-methods, or a method for equality-checking, ...var Barcode = Enum("Barcode", {/* factories */}, function(proto, key, value){ var _barcode = this; //so you can access the enum in closures, without the need for a "global" variable. //but if you mess around with this, you are the one to debug the Errors you produce. //this function is executed right after the prototpe-object for this enum-entry is created //and before any further modification. //neither this particular entry, nor the enum itself are done yet, so don't mess around with them. //the only purpose of this method is to provide you a hook //to add further properties to the proto-object //aou can also return an object with properties to add to the proto-object. //these properties will be added as configurable and writable but not enumerable. //and no getter or setter. If you need more control, feel free to modify proto on you own. return { isBarcode: function(){ return this instanceof _barcode; } }});//OR you can define it for every single property, //so you don't have to switch on the different properties in one huge functionvar Barcode = Enum("Barcode", {/* factories */}, { "UPCA": function(proto, key, value){ //same behaviour as the universal function //but will be executed only for the proto of UPCA var _barcode = this; //aka Barcode in this case var AP = []; return { //copy map and indexOf from the Array prototype map: AP.map, indexOf: AP.indexOf, //and add a custom toString and clone-method to the prototype toString: function(){ return "UPCA[ "+AP.join.call(this, ", ")+" ]"; }, clone: function(){ return _barcode.UPCA.apply(null, this); } }; }, //OR "QRCode": { //or simply define an object that contains the properties/methods //that should be added to the proto of QRCode //again configurable and writable but not enumerable substr: String.prototype.substr, substring: String.prototype.substring, charAt: String.prototype.charAt, charCodeAt: String.prototype.charCodeAt }});//mixin-functions and objects can be mixed