Tempo Enumerators (enumify)
Tempo uses a custom enumify utility to define enumerations rather than relying on native TypeScript enums. This gives consumers of the library access to a robust set of iteration and lookup methods out-of-the-box.
This guide explains how they are defined, how you use them as a consumer of the Tempo library, and why this design pattern was chosen.
1. How Tempo Enums are Defined
Tempo's core enumerators (like Weekdays, Months, Seasons) are built using the exported enumify function.
Under the hood, enumify returns a Proxy-wrapped, frozen object based on a null-prototype:
- Null-Prototype Root (
Object.create(null)): The base prototype is completely detached fromObject.prototype. Standard prototype properties liketoString,valueOf,constructor, orhasOwnPropertydo not exist on the object, eliminating key collision and prototype pollution risks. - Strict Runtime Immutability: The object is locked down with
Object.freeze()and wrapped in a read-onlyProxythat rejects writes, deletions, or runtime property additions. - Prototypal Extensibility: Even though each enum is deeply frozen and immutable, enums remain extensible through JavaScript prototypal inheritance via
.extend().
Array-based Definitions
When you pass an array to enumify, the strings become the keys, and the values are their zero-indexed positions:
export const WEEKDAY = enumify(['All', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']);
// Result: { All: 0, Mon: 1, Tue: 2 ... }Object-based Definitions
When you need specific string-to-string mappings, you pass an object literal directly:
export const SEASON = enumify({ Summer: 'summer', Autumn: 'autumn', Winter: 'winter', Spring: 'spring' });Type Inference
After defining the enumify object, simple TypeScript helper aliases pull out the types so you can use them reliably in function signatures:
export type SEASON = ValueOf<typeof SEASON>; // Type: 'summer' | 'autumn' | 'winter' | 'spring'
export type Season = KeyOf<typeof SEASON>; // Type: 'Summer' | 'Autumn' | 'Winter' | 'Spring'The above types allow for the use of Season and SEASON as type arguments, providing type safety for both the keys and values of the enum.
It is generally recommended to use the values (lowercase) as type arguments, as they are used as the actual values in the enumify object. This allows for easier use of the enumify methods, which operate on the values.
Note: TypeScript provides automatic typing for native enums, but it is not the case with enumify.
2. Using Enums Outside of Tempo
For consumers of the library, these enumerations are exposed via public package exports:
- As static properties on
Tempo(convenient in app code already usingTempo) - As the default export from
@magmacomputing/tempo/enums - As individual named exports from
@magmacomputing/tempo/enums
Avoid referencing internal source files (such as tempo.index.ts) in application code.
1. Static Properties on Tempo
You can access all enums via the unified Tempo.enums getter, or use top-level getters for core calendar and direction enums (WEEKDAY, WEEKDAYS, MONTH, MONTHS, DURATION, DURATIONS, SEASON, COMPASS):
import { Tempo } from '@magmacomputing/tempo';
// Core domain enums directly on Tempo:
const direction = Tempo.COMPASS.North; // 'north'
const monthIndex = Tempo.MONTH.Feb; // 2 (since 'All' was index 0)
// All enums via the unified Tempo.enums accessor:
const first = Tempo.enums.ORDINAL.first; // 1
const dateFormat = Tempo.enums.FORMAT.date;2. Canonical Namespace Import
Import the full enums object via the default export:
import enums from '@magmacomputing/tempo/enums';
const { COMPASS, MONTH } = enums;
const direction = COMPASS.North;
const monthIndex = MONTH.Feb;3. Direct Named Import
Import individual enumerators directly:
import { COMPASS, MONTH, WEEKDAY, ORDINAL, Enum } from '@magmacomputing/tempo/enums';
console.log('compass keys: ', Enum.keys(COMPASS));Static-First Reflection Suite (Enum.*)
In modern Tempo, enum reflection is performed using the Enum.* static helper suite, mirroring ECMAScript's standard Object.hasOwn and Object.keys:
import { Enum } from '@magmacomputing/tempo/enums';
// or: import { Enum } from '@magmacomputing/tempo/library';
// Iterating over properties (Type-safe & collision-proof)
const days = Enum.keys(Tempo.WEEKDAY); // readonly ('All' | 'Mon' | 'Tue' | ...)[]
const values = Enum.values(Tempo.WEEKDAY); // readonly (0 | 1 | 2 | ...)[]
const entries = Enum.entries(Tempo.WEEKDAY); // readonly (readonly [key, value])[]
const count = Enum.count(Tempo.WEEKDAY); // 8
// Validation
if (Enum.has(Tempo.SEASON, 'Spring')) { ... } // true if 'Spring' is a key
if (Enum.hasOwn(Tempo.SEASON, 'Spring')) { ... } // true if 'Spring' is a direct own key
if (Enum.includes(Tempo.SEASON, 'spring')) { ... } // true if 'spring' is a value
// Reverse lookups! Get the Key Name from the Value
const keyName = Enum.keyOf(Tempo.MONTH, 2); // 'Feb'
// Inversion & Aliasing support (last-key-wins)
const byCode = Enum.invert(Tempo.SEASON); // { summer: 'Summer', ... }
// Native iteration
for (const [key, val] of Tempo.WEEKDAY) {
console.log(key, val);
}3. Creating Custom Enums
You can utilize the same enumify engine for your own application logic by importing it from the library subpath. This is particularly useful for maintaining consistent data patterns and iteration capabilities throughout your project.
Basic Custom Enum
import { enumify, Enum } from '@magmacomputing/tempo/library';
// 1. Define your Enum
export const STATUS = enumify(['Pending', 'Active', 'Resolved', 'Archived']);
// 2. Use the static reflection helpers
const allKeys = Enum.keys(STATUS); // ['Pending', 'Active', 'Resolved', 'Archived']
const isActive = Enum.has(STATUS, 'Active'); // true
const value = STATUS.Resolved; // 2Extending Enums via Prototypal Inheritance (Enum.extend())
A standout capability of enumify is that enums are immutable yet extendable.
Developers typically assume that freezing an object (Object.freeze) prevents adding fields. However, enumify achieves non-destructive extensibility through JavaScript prototype delegation:
import { enumify, Enum } from '@magmacomputing/tempo/library';
// Base enum (frozen and immutable)
export const BASE_ROLE = enumify(['Viewer', 'Editor']);
// Extend with additional roles
export const ADMIN_ROLE = Enum.extend(BASE_ROLE, ['Admin', 'SuperAdmin']);
console.log(Enum.keys(ADMIN_ROLE)); // ['Viewer', 'Editor', 'Admin', 'SuperAdmin']
console.log(Enum.count(ADMIN_ROLE)); // 4
console.log(Enum.has(ADMIN_ROLE, 'Viewer')); // true (inherited from BASE_ROLE)
console.log(Enum.hasOwn(ADMIN_ROLE, 'Viewer')); // false (inherited, not an own property)
console.log(Enum.hasOwn(ADMIN_ROLE, 'Admin')); // true (direct own property on ADMIN_ROLE)How Prototypal Inheritance Works Here:
- Zero Mutation on Parent:
BASE_ROLEremains untouched and completely immutable. Existing code relying onBASE_ROLEis guaranteed never to see unwanted keys. - Prototype Delegation (
Object.create(this)): Callingextend()creates a new frozen child enum whose prototype is the parent enum. The child inherits all parent keys, values, and methods. - Stealth Traversal: Methods like
.keys(),.values(),.entries(),.count(), and evenJSON.stringify()seamlessly traverse the prototype chain to treat the extended enum as a unified collection. - Key Shadowing: Child enums can cleanly shadow inherited keys with new values without polluting the ancestor.
- Multi-tier Hierarchies: Enums can be extended multiple times (
BASE -> TENANT -> USER) while keeping each level individually sealed and safe.
4. How They Are Used Inside Tempo
Internally, the Tempo logic relies heavily on these enumerators. This gives the parsing and formatting engines guaranteed type-safety and robust lookup dictionaries.
For instance, the .format() logic can map tokens efficiently, and parser configuration (e.g., regex Snippet mapping) loops through them safely without manually declaring Object.keys() combinations everywhere.
The overarching design ensures the library stays strongly typed, internally consistent, and protected against accidental runtime mutation via Object.freeze().
5. enumify vs. TypeScript enum (The Trade-Offs)
TypeScript's native enum is one of the few TS features that generates structural runtime JavaScript, and it has known friction points in the JavaScript community.
Using enumify is a deliberate choice for high-quality library design.
The Wins for enumify
- Rich API (Methods): Native TS enums are plain JavaScript objects at runtime. To get the keys, you must write
Object.keys(MyEnum). Theenumifywrapper gives developers.keys(),.values(),.has(),.includes(),.map(),.filter(), and native iteration ([Symbol.iterator]). - Clean Namespace (Null-Prototype): Native enums and plain objects inherit from
Object.prototype, creating potential collisions with keys liketoString,valueOf, orconstructor.enumifyuses anullprototype, keeping the namespace completely unpolluted. - Prototypal Extensibility (
.extend()): TypeScript native enums cannot be extended, inherited, or composed.enumifyallows enums to inherit from base enums cleanly without mutating the parent. - Predictable Serialization: TypeScript numeric enums generate strange "reverse mappings" in compiled JS (e.g.,
{ 0: "Up", "Up": 0 }). This makes iterating over them or stringifying them to JSON very messy.enumifyobjects serialize cleanly and safely. - Immutability:
enumifyfreezes the object at runtime (Object.freeze) and seals it with aProxy. Standard TS enums can technically be mutated at runtime by malicious or sloppy ES code. - NodeJS/ESM Compatibility: Standard TS enums can cause friction with isolated module compilers (like Vite or esbuild) or when importing into vanilla JS.
enumifygenerates 100% standard ES2015 JavaScript.
The Losses (Trade-offs) for enumify
- Slightly More Boilerplate Definition: Defining an
enumifydictionary takes 2-3 lines of code (exporting the const, then exporting thetypealias). TS native enums do both (value and type) as part of theenumkeyword. - Missing Nominal Typing: TypeScript native enums offer "nominal" typing (e.g.,
enum A { X }cannot be passed to a function expectingenum B { X }even if the keys/structures match).enumifyrelies on structural typing (union of literals), meaning TypeScript allows passing the raw string'spring'into a function rather than forcing you to strictly useTempo.SEASON.Spring. - Slight Runtime Overhead: Instantiating the proxy/prototype wrapper and freezing it adds a microscopic runtime cost compared to evaluating a plain object literal, though parsing the library is typically a one-time engine cost.
- More Verbose Setup: TypeScript's enum can use auto-incrementing numeric values, but
enumifyrequires explicit values for each key.