This is a file that describes the use of enum.mjs in it's current raw form, as if you simpy inserted it into your codebase.
To avoid repeating myself, this will be a verbose recap of what my code does, while my codebase combined with jsdoc will tell you how.
It's a parent global Enum class (javascript already has Object, String etc.), used to create new enums.
In the case of this feature prototype, it is not only constistently enumerable but also iterable.
Different types of enums provide different benefits, but all of them are immutable.
If you are questioning my design decisions, here are my sources:
Note
When I say "like in x language", I mean I borrowed some ideas from the language, and made it fit into javascript.
I have been writing vanilla javascript for a long time now.
I don't posess the same level of expertise to implement an exact copy of the Enum from another language.
And either way - I don't have to do that.
Public properties:
-
Static:
.flags- control tag1, enables creation of Enum structured as a collection of flags.symbolic- control tag1, enables creation of Enum with EnumVariants that hold values.simple- control tag1, default, enables creation of a simple C style Enum.types- table of transformators for types2 used in the schema3 of EnumVariants.check()- validator function for the keys of a structure object4, fails if:- the keys cannot be considered valid variable names5 (e.g.
const 57or<obj>.*fish) - or they collide with already existing properties (e.g
<enum>['[[metadata]]']or<obj>.constructor)
- the keys cannot be considered valid variable names5 (e.g.
.match(callbacks, <EnumVariant>)- pattern matching6 function, imitates thematchexpression in Rust- it accepts callbacks for all possible variants and runs the appropriate function once it finds the match
- the function will be given the value of the matched variant
- throws if you do not have a function for every possible variant
.checkType()- checks if a string represents one of the types2 used in a schema3
-
Instance:
.values(<int>)- returns an array of names matching the bitmask7<arr>.printreturns a CCS (comma separated string) of all valuesEnum.simpleandEnum.flagsonly
.keys(<int>)- returns an array of integers matching the bitmask7<arr>.printreturns a CCS of all keysEnum.simpleandEnum.flagsonly
.entries(<int>)- returns an array of[value:key]pairs matching the bitmask7<arr>.printreturns a CCS of all entries, formatted as<value>(<key>),Enum.simpleandEnum.flagsonly
<enum>.match =- non-enumerable, sets a default object of callbacks for pattertn matching6 on this Enum instanceEnum.symboliconly
<enum>.match()- shortcut toEnum.match(), that uses default callbacksEnum.symboliconly
<enum>.size- same asObject.keys(<enum>).length, may be useful for bitmasking7Enum.simpleandEnum.flagsonly
Private properties:
-
Instance:
.#form- object of default callbacks for pattern matching6
Important
Enum can only be initialized with a {} object, it does not accept level-0 arrays
(i.e. array as a structure object4 of the enum)
A subclass of Enum designed to serve as a constructor in the fields of Enum.symbolic.
Public properties:
- Static:
- Instance:
<enumvar>.match =- non-enumerable, sets new matching function for the relevant EnumVariant- not available on constructed variant instances
Private properties:
- Inherited
Caution
Manually creating EnumVariants can lead to unexpected problems!
❌ Do not create new EnumVariant()
✔️ Do use bound builder() functions placed in the Enum
❔ Can use static methods of class EnumVariant
An EnumVariant is either prebuilt or constructed by invoking an enum field closure with <enum>.<prop>(<val>)
The structure of the EnumVariant is decided at the time of Enum declaration.
.valueis a default field used for single value Enum fields- if the enum field contained a complex value11 at the time of declaration
- all instances will follow it's shape and types2
.valuewill not be added by default
- EnumVariant can handle both
{}and[]objects
If an Enum field contained a valid type2 or a complex value11 a bound constructor function builder() is placed there, otherwise an EnumVariant containing a Symbol is placed.
<builder>.schema- a copy of the schema3 used by the builder<builder>.match =- updates the default matching callback for the relevant EnumVariant
Is a non-enumerable field (object) present in every Enum and EnumVariant.
It contains mechanically important information and changes structure based on the kind of Enum.
metadata.titleandmetadata.typeoccur in all types of Enummetadata.parentoccurs in all instances of EnumVariant
It is named this way for 2 reasons:
- that's how runtime-only properties look in dev tools (i.e.
[[PrimitiveValue]]of aNumber) - it avoids collisions since
[is an invalid character for a variable name
A simple C style enum, each field gets an integer value associated with it.
Its main purpose is to allow programmers to use readable constants, that translate to a primitive integer value (compiler benefit).
It also allows you to define fusions (i.e. 5 or 101 is a fusion of 4|1).
To create an Enum give it:
- a name string,
- a structure object4,
- undefined or
Enum.simple
To access any field in the enum:
- write
enum.<prop>- manual access by field name is designed to imitate C# casting field to integer, via
enum.<prop>.int
- manual access by field name is designed to imitate C# casting field to integer, via
- or
enum[<int>]- manual access by integer is is designed to imitate C# casting enum to integer, via
enum[<int>]
- manual access by integer is is designed to imitate C# casting enum to integer, via
- or
enum[enum.<prop>.int], but that is unnecessary, and this format can just keep getting nested
The structure object4 is only accepted with fields that pass the validation, and any value that is not a valid number is ignored.
By default each field's integer is assigned automatically, based on the order of insertion.
You can override the integer value with a valid number. Example:
const daysOfWeek = new Enum('daysOfWeek', {
Monday: '',
Tuesday: 0,
Wednesday: Infinity,
Thursday: { a: true, b: 78 },
Friday: (c) => {
console.log(c);
},
Saturday: -57,
Sunday: '4'
});Mondayis automatically assigned0Tuesdaymanually takes the spot ofMonday,Mondayis moved to1- every other field is automatically assigned numbers
2-6 - except that
Saturdayis assigned-57and therefore5doesn't exist
A sorting mechanism will ensure that you do not create duplicate integer associations, and do not overwrite any of the fields.
Every field in the enum becomes a string value. For each field, 2 properties are defined on the resulting enum:
enum.<prop>is non-enumerable, and will contain aString(<prop>)- that string object will also posess an
.intproperty
- that string object will also posess an
enum[<int>]is enumerable, and will contain a primitive string of<prop>- this way integers are keys, and field names are values in any enumeration operation (i.e.
Object.keys()) - the enum will be enumerated in the correct order, independent of the field names
- this way integers are keys, and field names are values in any enumeration operation (i.e.
log(daysOfWeek.Monday); // String('Monday')
log(daysOfWeek.Monday.int); // 1
log(daysOfWeek[0]); // Tuesday
for (const day in daysOfWeek) {
log(day); // 0,1,2,3,4,6,-57
}
for (const val of daysOfWeek) {
log(val); // Tuesday ... Saturday
log(daysOfWeek[val]); // String('Tuesday') ... String('Saturday')
log(daysOfWeek[val].int); // 0 ... -57
}A C# [Flags] style enum structured as a collection of flags.
It is similar to Enum.simple, but its main purpose is to have clearly defiend, individual bit fields.
It is structured as a collection of flags.
It also allows you to define supersets (i.e. 8 is one bit 1000, whilst 7 is a superset 0111 of bits 4|2|1)
To create an Enum give it:
- a name string,
- a structure object4,
Enum.flags
To access any field in the enum:
- write
enum.<prop>- manual access by field name returns an integer that is always a standalone flag
- or
enum[<int>]- manual access by integer is is designed to imitate C# casting enum to integer, via
enum[<int>]
- manual access by integer is is designed to imitate C# casting enum to integer, via
The structure object4 is only accepted with fields that pass the validation, and any value is ignored12.
Each field's integer is assigned automatically, based on the order of insertion.
Consider example similar to the previous object:
const daysOfBits = new Enum(
'daysOfBits',
{
0: (a) => {
return `oops, empty for bitmask ${a}`;
},
Monday: '',
Tuesday: 0,
Wednesday: Infinity,
Thursday: { a: true, b: 78 },
Friday: (c) => {
console.log(c);
},
Saturday: -57,
Sunday: null
},
Enum.flags
);Mondayis automatically assigned1Tuesdayis automatically assigned2Thursdayis automatically assigned8- enum integers are different bitshifts of
1that end at-2147483648- this is done to simplify bit loops and individual bit access, since you can simply write
1<<i -2147483648is simply a negative of the 32nd bit- the enum is limited to only 32 fields because javacript bitwise operators only deal with 32bit numbers
- this is done to simplify bit loops and individual bit access, since you can simply write
- value from the field
0in the structure object is not ignored- it is expected to be a function
- the resulting enum will contain a non-enumerable property
0 - by default
<enum>[0]()will be a function that returnsundefined
Every field in the enum becomes a string value. For each field, 2 properties are defined on the resulting enum:
enum.<prop>is non-enumerable, and will contain anumberprimitive8 of<int>enum[<int>]is enumerable, and will contain a string primitive8 of<prop>- this way integers are keys, and field names are values in any enumeration operation (e.g.
Object.keys())
- this way integers are keys, and field names are values in any enumeration operation (e.g.
log(daysOfBits.Monday); // 1
log(daysOfBits[8]); // Thursday
log(daysOfBits.entries(18 & 7)); // ['Tuesday']
for (const day in daysOfBits) {
log(day); // 1,2,4,8,16,32,64
log(daysOfBits[day]); // Monday ... Sunday
}
log(daysOfBits.values(1 << 26)); // oops, empty for bitmask 67108864A Rust style enum that doesn't have integer associations and instead holds constructors or identifiers.
This particular enum was much too complicated to implement so it likely doesn't work the same way as in Rust.
Its main purpose is to introduce a collection of constructors/signature values, that allow it to hold different values and pattern match6.
To create an Enum give it:
- a name string,
- a structure object4,
Enum.symbolic
Property access it a little bit more complicated.
- fields with a unique identifier
- such field will be an EnumVariant accessible on the Enum itself
- to access the value write
<enum>.<prop>.value
- fields with a single value constructor
- first you will have to construct an EnumVariant instance via
<enum>.<prop>(<val>) - to access the value write
<instance>.value- you can also access the constructor's schema3 via
<enum>.<prop>.schema
- you can also access the constructor's schema3 via
- first you will have to construct an EnumVariant instance via
- fields with a complex11 value constructor
- first you will have to construct an EnumVariant instance via
<enum>.<prop>(<val>) - such instances do not have a default field
.value - to access any value you can ask for any property that was on the structure object4
- you can also access the constructor's schema3 via
<enum>.<prop>.schema
- you can also access the constructor's schema3 via
- first you will have to construct an EnumVariant instance via
The structure object4 is only accepted with fields that pass validation, and the values are parsed.
Keep in mind that I had to adjust the idea of this kind of enum to play nice with javascript.
Here is an example from the rust-by-example page, with some modifications:
const WebEvent = new Enum(
'WebEvent',
{
PageLoad: 'constructor',
PageUnload: 1,
KeyPress: 'char',
Paste: 'string',
Click: { x: 'int', y: 'int', z: [null, 'bool', Symbol(`should be a str`)] }
},
Enum.symbolic
);Each field will eventually be an EnumVariant.
It can either be prebuilt, or await input to create a new instance.
Let's break down the different kinds of fields you can have:
- Unique identifier, used for references:
A field in the enum may be treated as just a constant. This is the case when you declare a field with a value that is neither an object nor a type2.
Example:WebEvent.PageLoadwas declared with an invalid value- the field name is used to create a
Symbol- this way 2 identical fields from 2 diferent enums are not equal, but their descriptions are
WebEvent.PageLoadis now a prebuilt EnumVariant
- Single value constructor:
A field in the enum can be declared with just a type2.
In that case, it becomes a constructor of single values, usually primitive8.
Example:WebEvent.KeyPresswas declared with'char', which is one of the available types2- schema3 is built for the singular type annotation
- it looks like this
{ value: 'char' }
- it looks like this
WebEvent.KeyPressis now a bound builder()- it will accept a value and, in standard javascript fashion, try to convert it
- this will create an instance of EnumVariant
- you can keep creating more
WebEvent.KeyPressinstances
- Complex constructor:
A field in the enum can be declared with an object containing one or more fields.
In that case, it becomes a constructor of objects.
Each fields value is expected to be either a type2 or an object; in case it is neither, it's treated as a'str'type2
Example:WebEvent.Clickis declared with an object- the object is recursively processed and a schema3 is built
- it loks like this
{ x: 'int', y: 'int', z: ['str', 'bool', 'str'] }
- it loks like this
WebEvent.Clickis now a bound builder()- it will accept an object of values and, in standard javascript fashion, try to convert them
- this will create an instance of EnumVariant
- you can keep creating more
WebEvent.Clickinstances
const load = WebEvent.PageLoad;
const unload = WebEvent.PageUnload;
const press = WebEvent.KeyPress('xfgh');
const paste = WebEvent.Paste('hello');
const click = WebEvent.Click({
x: 59.92,
y: '77.1',
z: [false, true, { location: 'here' }]
});Here are the resulting values, all of which are instances of EnumVariant:
loadload.valueis aSymbol(PageLoad)
unloadunload.valueis aSymbol(PageUnload)
presspress.valueis'x'
pastepaste.valueis aString('hello')
click- does not have a default property
.valuebut could have.valueif declared by the user .xis59.yis77.zis an array of['false', true, '[object Object]']
- does not have a default property
const permissions = new Enum('permissions', {
Read: 1,
Delete: 2,
Write: 4,
Mod: 5,
Admin: 7
});
const permissionsBits = new Enum(
'permissionsBits',
{
Read: 1,
Delete: '',
Write: true,
Mod: 5,
Admin: 99
},
Enum.flags
);
log(permissions[permissions.Read.int | permissions.Write.int]); // Mod
log(permissions[permissions.Read.int | permissions.Write.int | permissions.Delete.int]); // Admin
log(
permissionsBits[1 + (permissionsBits.Read | permissionsBits.Write | permissionsBits.Delete)] // Mod
);
const mod = permissionsBits.Read | permissionsBits.Write; // 5
log(permissions.values(7)); // ['Read, Delete, Write']
log(permissions.entries(7).print); // 'Read, Delete, Write'
log(permissionsBits.keys(mod)); // [1, 4]
log(permissionsBits.values(1 << 15)); // undefined
// Admin is it's own bit, Admin-1 is a superset of preceding bits
const admin =
permissionsBits.Read | permissionsBits.Delete | permissionsBits.Write | permissionsBits.Mod; // 15
const allFlags = permissionsBits.size ** 2 - 1; // 24
const adminBit = (permissionsBits.size - 1) ** 2 - 1; // 15
log(adminBit === permissionsBits.Admin - 1); // true
if ((adminBit | permissionsBits.Delete) === permissionsBits.Admin - 1) {
// Admin-1 contains Delete bit so adding it should change nothing
log('is Admin');
if ((admin & allFlags) === allFlags) {
// Admin or Admin-1 does not stand above Admin
// No one field can represent all flags of the Enum
log('is Above All');
}
} else {
log('is Mod');
}WebEvent.match = {
PageLoad(val) {
log(val.description);
return 8;
},
PageUnload(val) {
log(val.description);
},
KeyPress(val) {
log(val);
},
Paste(val) {
log(val);
},
Click(obj) {
log(obj.z);
}
};
log(WebEvent.Click.schema); // {x: 'int', y: 'int', z: ['str', 'bool', 'str']}
log(WebEvent.match(load)); // logs PageLoad, returns and logs 8
WebEvent.PageLoad.match = (v) => {
log(v.description + 5886);
};
log(WebEvent.match(load)); // logs PageLoad5886, returns and logs undefined
WebEvent.Click.match = (obj) => {
return obj.x;
};
log(WebEvent.match(click)); // 59
for (const key in click) {
log(key); // x, y, z
}
for (const val of click) {
log(val); // 59, 77, ['false', true, '[object Object]']
}The details of methods and structures here described are pretty self explanatory.
Once you look at the code, and run examples.js a couple of times with debugger, you'll get it.
Feedback on the syntax, memory efficiency, bugs is welcome in the issues section.
I will deploy it as a package, but consider this an experiment, because I've had to take some roundabouts:
- for convenience's sake I decided to implement it using latest features like private and static class fields from ES 2022
- I had to "manifest" certain data that should ideally be dealt with by the ts compiler or by the runtime
- I tried mimicking your average enum syntax best I could, but it's not completely convenient
[[metadata]].nameexists simply for debugging purposes as the code can't read itself and know how you named the newly created Enum- metadata object in general is a form of introspection, necessary for a functional "feature prototype"
- one or more loops are often used to parse the Enum structure object4 and then to parse source objects10
However I encourage people to try out this Enum implementation.
I believe this code can be rewritten to fit old enough versions of ECMAScript standard.
This project is under the permissive free license.
Footnotes
-
Control tags are symbols used to change the functionality and output of the Enum constructor ↩ ↩2 ↩3
-
Types in a schema3 tell the EnumVariant constructor how to transform values passed in ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11
-
Schema is an object that acts as the blueprint for new EnumVariants, it defines the structure and the types2 of the values ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10
-
Structure object is the input object used to define the layout and values of the new Enum or layout and types2 of the new EnumVariant ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12
-
By variable naming rules I mean legal syntax, for more detail see Identifiers ↩
-
Enum.match()function works like aswitchstatement, that is an expression, that forces you to consider every possible EnumVariant case for a certain Enum ↩ ↩2 ↩3 ↩4 -
A number can be used as a set of binary flags where bit
1/0istrue/false. In this case each bit corresponds to an individual field in the Enum (e.g11is binary1011or fourth, second, first fields) ↩ ↩2 ↩3 ↩4 ↩5 -
Primitive value is a non-extensible value, without properties, which is not an instance. Example:
typeof 'hello' === 'string'is true and'hello' instanceof Stringis falsetypeof new String('hello') === 'string'is false andnew String('hello') instanceof Stringis true- FTPS9 we can ignore the fact that javascript primitives are not fully primitive, for reference
- FTPS9
Symbols act similarly to primitives
-
For This Project's Scope - for the logic, functionality, use case, and general idea behind this code ↩ ↩2
-
Source object is similar to the structure object4 but it's values are transformed according to the schema3 and a new EnumVariant instance is built ↩ ↩2
-
Complex value is the opposite of a primitive8, usually an object or function ↩ ↩2 ↩3
-
field
0is reserved for a default action from bitmasking7 methods (i.e.<enum>.values()) if they fail to find a match ↩