How to implement Swift-like enums with associated values in JavaScript? How to implement Swift-like enums with associated values in JavaScript? javascript javascript

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