JSPM

@glitchproof/enumerator

0.1.2
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 3
  • Score
    100M100P100Q35009F
  • License MIT

> Type-safe enumerations with metadata, nesting, and built-in type guards.

Package Exports

  • @glitchproof/enumerator
  • @glitchproof/enumerator/package.json

Readme

@glitchproof/enumerator

Type-safe enumerations with metadata, nesting, and built-in validation.

import { enumerate } from "@glitchproof/enumerator";

const Status = enumerate({
  ACTIVE: ["active", { icon: "✅", color: "green" }],
  INACTIVE: ["inactive", { icon: "⭕", color: "gray" }],
  PENDING: "pending",
});

Status.ACTIVE.value; // 'active'
Status.ACTIVE.meta.icon; // '✅'
Status.ACTIVE.equals("active"); // true (type guard!)
Status.contains(userInput); // Runtime validation

Features

  • Type-safe - Full TypeScript inference and type narrowing
  • Metadata - Attach rich data to each value
  • Nesting - Organize enums hierarchically
  • Type guards - equals(), contains(), containsOneOf()
  • Validation - Runtime checking with type narrowing
  • Immutable - All values frozen with Object.freeze()
  • Performant - Lazy evaluation with caching

Installation

npm install @glitchproof/enumerator

Quick Start

import { enumerate } from "@glitchproof/enumerator";

// Simple values
const Status = enumerate({
  ACTIVE: "active",
  INACTIVE: "inactive",
  PENDING: "pending",
});

Status.ACTIVE.value; // 'active'
Status.ACTIVE.equals(userInput); // Type guard
Status.contains(apiResponse); // Validation

// With metadata
const HTTP = enumerate({
  OK: [200, { message: "Success", retry: false }],
  ERROR: [500, { message: "Server error", retry: true }],
});

HTTP.OK.value; // 200
HTTP.OK.meta.message; // 'Success'

// Nested
const API = enumerate({
  V1: { USERS: "/v1/users", POSTS: "/v1/posts" },
  V2: { USERS: "/v2/users", POSTS: "/v2/posts" },
});

API.V1.USERS.value; // '/v1/users'

// Or wrap existing TypeScript enums
enum LegacyStatus {
  ACTIVE = "active",
  PENDING = "pending",
}
const Legacy = enumerate(LegacyStatus);

Legacy.ACTIVE.value; // 'active'
Legacy.contains("active"); // true

API

Individual Values

Status.ACTIVE.value; // 'active' - The enum value
Status.ACTIVE.meta; // { icon: '✅' } - Metadata (if defined)
Status.ACTIVE.equals(x); // boolean - Type guard (uses Object.is)

Enum Object

Validation:

Status.contains(value); // Check if value exists
Status.containsOneOf(value, ["active", "pending"]); // Check subset (array)
Status.containsOneOf(value, "active", "pending"); // Check subset (rest params)
Status.containsOneOf(value, (s) => s.ACTIVE); // Check subset (callback)
Status.containsOneOf(value, (s) => [s.ACTIVE, s.PENDING]); // Check subset (callback)

Access:

Status.values; // ['active', 'inactive', 'pending'] - Frozen array, also type is a tuple of values like ['active', 'inactive', 'pending']  same as value
Status.asType; // { ACTIVE: 'active', ... } - Key-value object (lazy, frozen)
Status.asValueType; // 'active' | 'inactive' |'pending' - only for type , runtime value is a null

Types

InferType and InferTypeAsUnion are utility types to extract types from enumerated objects.

  • InferType<EnumObj> - Extracts the asType property type (key-value object)
  • InferUnionType<EnumObj> - Extracts the asValueType property type (union of values)
import {
  enumerate,
  type InferType,
  type InferUnionType,
} from "@glitchproof/enumerator";

const HTTP_CODES = enumerate({
  SUCCESS_CODES: {
    SUCCESS: 200,
    CREATED: 201,
  },
  ERROR_CODES: {
    BAD_REQUEST: [
      400,
      { message: "Bad Request", retry: false, category: "client_error" },
    ],
    UNAUTHORIZED: [
      401,
      { message: "Unauthorized", retry: false, category: "client_error" },
    ],
    FORBIDDEN: [
      403,
      { message: "Forbidden", retry: false, category: "client_error" },
    ],
    NOT_FOUND: [
      404,
      { message: "Not Found", retry: false, category: "client_error" },
    ],
    SERVER_ERRORS: {
      INTERNAL_SERVER_ERROR: [
        500,
        {
          message: "Internal Server Error",
          retry: true,
          category: "server_error",
        },
      ],
      BAD_GATEWAY: [
        502,
        { message: "Bad Gateway", retry: true, category: "server_error" },
      ],
      SERVICE_UNAVAILABLE: [
        503,
        {
          message: "Service Unavailable",
          retry: true,
          category: "server_error",
        },
      ],
    },
  },
});

// InferType - extracts asType (key-value object)
type ErrorCodesType = InferType<typeof HTTP_CODES.ERROR_CODES>;
// Result: { BAD_REQUEST: 400; UNAUTHORIZED: 401; FORBIDDEN: 403; NOT_FOUND: 404 }
// Note: SERVER_ERRORS is not included (it's nested)

// InferTypeAsUnion - extracts asValueType (union of values)
type ErrorCodesUnion = InferTypeAsUnion<typeof HTTP_CODES.ERROR_CODES>;
// Result: 400 | 401 | 403 | 404
// Note: Nested values (500, 502, 503) are not included

// Alternative: Direct property access (same result)
type ErrorCodesTypeAlt = typeof HTTP_CODES.ERROR_CODES.asType;
type ErrorCodesUnionAlt = typeof HTTP_CODES.ERROR_CODES.asValueType;

type HttpCodes = InferType<typeof HTTP_CODES>; // Returns `never` because HTTP_CODES has no direct enum values, only a container of nested enumerator objects

Iteration:

// Callbacks receive { value, meta } objects
Status.forEach(({ value, meta }, idx, items) => {
  console.log(value, meta?.icon); // meta is undefined if not defined
});

Status.map(({ value, meta }) => ({
  label: value,
  icon: meta?.icon || "?",
}));

Configuration:

enumerate(definition, { maxDepth: 5 }); // Limit nesting (default: 10)

Examples

API Error Handling

const APIError = enumerate({
  NETWORK: ["network", { message: "Connection failed", retry: true }],
  AUTH: ["auth", { message: "Please login", retry: false }],
  RATE_LIMIT: [
    "rate_limit",
    { message: "Too many requests", retry: true, wait: 60 },
  ],
  SERVER: ["server", { message: "Server error", retry: true }],
});

async function fetchWithRetry(url: string) {
  try {
    return await fetch(url);
  } catch (error) {
    const errorType = classifyError(error);

    APIError.forEach(({ value, meta }) => {
      if (value === errorType && meta.retry) {
        setTimeout(() => fetchWithRetry(url), meta.wait || 1000);
      }
    });
  }
}

Theme Design Tokens

const Color = enumerate({
  PRIMARY: ["#3b82f6", { name: "Blue", dark: "#1e40af" }],
  SUCCESS: ["#22c55e", { name: "Green", dark: "#15803d" }],
  DANGER: ["#ef4444", { name: "Red", dark: "#b91c1c" }],
  WARNING: ["#f59e0b", { name: "Amber", dark: "#b45309" }],
});

function getThemeColor(color: string, isDark: boolean) {
  let result = color;
  Color.forEach(({ value, meta }) => {
    if (value === color) {
      result = isDark ? meta.dark : value;
    }
  });
  return result;
}

// Generate CSS variables
const cssVars = Color.map(
  ({ value, meta }) => `--color-${meta.name.toLowerCase()}: ${value};`,
).join("\n");

Workflow States

const TaskStatus = enumerate({
  TODO: ["todo", { next: ["in_progress"], canEdit: true }],
  IN_PROGRESS: ["in_progress", { next: ["review", "todo"], canEdit: true }],
  REVIEW: ["review", { next: ["done", "in_progress"], canEdit: false }],
  DONE: ["done", { next: [], canEdit: false }],
});

function canTransition(current: string, next: string): boolean {
  let valid = false;
  TaskStatus.forEach(({ value, meta }) => {
    if (value === current) {
      valid = meta.next.includes(next);
    }
  });
  return valid;
}

TypeScript

Full type inference and narrowing:

const Status = enumerate({
  ACTIVE: ["active", { icon: "✅" }],
  INACTIVE: "inactive",
});

// Type narrowing with guards
function handle(status: unknown) {
  if (Status.ACTIVE.equals(status)) {
    status; // Narrowed to: 'active'
  }

  if (Status.contains(status)) {
    status; // Narrowed to: 'active' | 'inactive'
  }
}

// Extract types
type Value = typeof Status.ACTIVE.value; // 'active'
type Meta = typeof Status.ACTIVE.meta; // { icon: '✅' }

Advanced

Filter by Metadata

const Features = enumerate({
  ANALYTICS: ["analytics", { enabled: true }],
  AI_CHAT: ["ai_chat", { enabled: false }],
  EXPORT: ["export", { enabled: true }],
});

const enabled = Features.map((item) => item)
  .filter((item) => item.meta?.enabled)
  .map((item) => item.value);
// ['analytics', 'export']

Edge Cases

.equals() uses Object.is() for correct NaN and ±0 handling:

const Nums = enumerate({ ZERO: 0, NEG_ZERO: -0, NAN: NaN });

Nums.ZERO.equals(0); // true
Nums.ZERO.equals(-0); // false (distinguishes +0 from -0)
Nums.NAN.equals(NaN); // true (handles NaN correctly)

Best Practices

// ✅ Keep metadata consistent
const Status = enumerate({
  ACTIVE: ["active", { icon: "✅", color: "green" }],
  INACTIVE: ["inactive", { icon: "⭕", color: "gray" }],
});

// ✅ Limit nesting depth (2-3 levels)
const API = enumerate({
  V1: { USERS: "/v1/users" },
  V2: { USERS: "/v2/users" },
});

// ✅ Use for external configs only
const config = { ACTIVE: "active" } as const;
const Status = enumerate(config);

Migration from TS Enums

Option 1: Wrap Existing Enums

// Existing TypeScript enum
enum Status {
  ACTIVE = "active",
  INACTIVE = "inactive",
  PENDING = "pending",
}

// Wrap it directly - gets all enumerator features!
const StatusEnum = enumerate(Status);

StatusEnum.ACTIVE.value; // 'active'
StatusEnum.ACTIVE.equals("active"); // true (type guard!)
StatusEnum.contains(userInput); // Runtime validation
StatusEnum.values; // ['active', 'inactive', 'pending']

Best for: Gradual migration of legacy code.

Note: Works best with string enums. Numeric enums have reverse mappings that create extra entries.

Option 2: Replace with Enumerator

// Before
enum Status {
  ACTIVE = "active",
}
const meta = { [Status.ACTIVE]: { icon: "✅" } };

// After
const Status = enumerate({
  ACTIVE: ["active", { icon: "✅" }],
});

Best for: New code or full refactors.

Changes: Status.ACTIVEStatus.ACTIVE.value, metadata now inline, validation built-in.

Why This Package?

Better than TypeScript enum:

  • ✅ Metadata support
  • ✅ Runtime validation
  • ✅ Type guards included
  • ✅ Nested structures
  • ✅ Actually immutable

Better than as const:

  • ✅ Built-in validation
  • ✅ Type guards
  • ✅ Iteration helpers
  • ✅ Metadata support

When NOT to use:

  • Zero dependencies required (company policy)
  • Ultra-simple cases (3 values, no metadata)
  • Need reverse numeric mapping

Performance

  • Lazy evaluation (zero overhead if unused)
  • Aggressive caching (compute once, use forever)