Super‑lightweight Rust‑style tagged unions for TypeScript — fully type‑safe, zero‑dependency, ~1 kB min+gz.
IronEnum lets you model expressive enums (a.k.a. tagged unions) in plain TypeScript and gives you ergonomic helpers inspired by Rust’s Option, Result, and try patterns.
- Features
 - Installation
 - Quick Start
 - Core Concepts
 - Built-in Types
 - EcoSystem Helpers
 - Advanced Usage
 - API Reference
 - Best Practices
 - Examples
 
- 🦀 Rust-inspired - Familiar 
Result,Option, and pattern matching - 🎯 Type-safe - Full TypeScript support with excellent type inference
 - 🚀 Zero dependencies - Lightweight and fast (~1kb gzipped)
 - 🔧 Ergonomic API - Intuitive constructors and method chaining
 - 🎮 Pattern matching - Exhaustive 
matchandmatchAsyncmethods - 🛡️ Error handling - Built-in 
TryandTryIntoutilities 
npm install iron-enum
# or
yarn add iron-enum
# or
pnpm add iron-enumimport { IronEnum } from 'iron-enum';
// Define your enum variants
const Status = IronEnum<{
  Loading: undefined;
  Ready: { finishedAt: Date };
  Error: { message: string; code: number };
}>();
// Create instances
const loading = Status.Loading();
const ready = Status.Ready({ finishedAt: new Date() });
const error = Status.Error({ message: "Network error", code: 500 });
// Pattern match
const message = ready.match({
  Loading: () => "Still working...",
  Ready: ({ finishedAt }) => `Done at ${finishedAt.toLocaleTimeString()}`,
  Error: ({ message }) => `Failed: ${message}`
});
// Use in UI components...
// Works with React and SolidJS out of the box!
const component = (props) => {
    const [value, setValue] = useState(Status.Loading());
    return (
        <div>
            {value.match({
                Loading: () => <div>Loading</div>,
                Ready: ({ finishedAt }) => <div>{`Done at ${finishedAt.toLocaleTimeString()}`}</div>,
                Error: ({ message }) => <div>{`Failed: ${message}`}</div>
            })}
        </div>
    )
}IronEnum uses TypeScript's type system to create discriminated unions with zero runtime overhead:
// Simple enum without payloads
const Direction = IronEnum<{
  North: undefined;
  South: undefined;
  East: undefined;
  West: undefined;
}>();
// Enum with different payload types
const UserEvent = IronEnum<{
  Login: { userId: string; timestamp: Date };
  Logout: { userId: string };
  Update: { userId: string; changes: Record<string, any> };
}>();
// Using the enum
const event = UserEvent.Login({ 
  userId: "user123", 
  timestamp: new Date() 
});The match method ensures exhaustive handling of all variants:
const Shape = IronEnum<{
  Circle: { radius: number };
  Rectangle: { width: number; height: number };
  Triangle: { base: number; height: number };
}>();
const shape = Shape.Circle({ radius: 5 });
const area = shape.match({
  Circle: ({ radius }) => Math.PI * radius ** 2,
  Rectangle: ({ width, height }) => width * height,
  Triangle: ({ base, height }) => (base * height) / 2
});
// With fallback using '_'
const description = shape.match({
  Circle: () => "Round shape",
  _: () => "Polygonal shape"  // Catches Rectangle and Triangle
});Use if and ifNot for conditional logic:
const Auth = IronEnum<{
  Authenticated: { user: { id: string; name: string } };
  Anonymous: undefined;
}>();
const auth = Auth.Authenticated({ user: { id: "123", name: "Alice" } });
// Simple boolean check
if (auth.if("Authenticated")) {
  console.log("User is logged in");
}
// With callbacks
const userName = auth.if(
  "Authenticated",
  ({ user }) => user.name,
  () => "Guest"
);
// Inverse check
auth.ifNot(
  "Anonymous",
  () => console.log("User is authenticated")
);Rust-style error handling:
import { Result, Ok, Err } from 'iron-enum';
const DivideResult = Result<number, string>();
// 2. Use the factory's type for the return annotation
function divide(a: number, b: number): typeof DivideResult._.typeOf {
  if (b === 0) {
    return DivideResult.Err("Division by zero");
  }
  return DivideResult.Ok(a / b);
}
const result = divide(10, 2);
// Pattern matching
const message = result.match({
  Ok: (value) => `Result: ${value}`,
  Err: (error) => `Error: ${error}`
});
console.log(message); // "Result: 5"
// Convenience methods
console.log(result.isOk());        // true
console.log(result.unwrap());      // 5
console.log(result.unwrap_or(0));  // 5Nullable value handling:
import { Option } from 'iron-enum';
// Assumes 'User' type is defined elsewhere
type User = {id: number, name: string};
const optUser = Option<User>();
const userOption = optUser.Some({ id: 123, name: "Alice" }); // Example
// Convert to Result
const userResult = userOption.ok_or("User not found");
// Pattern matching
userOption.match({
  Some: (user) => console.log(`Found: ${user.name}`),
  None: () => console.log("User not found")
});
// Convenience methods
console.log(userOption.isSome());           // boolean
console.log(userOption.unwrap_or(null));    // User | nullAutomatic exception handling:
import { Try, TryInto } from 'iron-enum';
// Wrap a potentially throwing function
const result = Try.sync(() => {
  return JSON.parse('{"valid": "json"}');
});
// Async version
const asyncResult = await Try.async(async () => {
  const response = await fetch('/api/data');
  return response.json();
});
// Transform existing functions
const safeParse = TryInto.sync(JSON.parse);
// const safeReadFile = TryInto.async(fs.promises.readFile); // Example for Node.js
// Use the wrapped functions
const parseResult = safeParse('{"key": "value"}');
parseResult.match({
  Ok: (data) => console.log("Parsed:", data),
  Err: (error) => console.log("Parse failed:", error)
});For runtime validation (e.g., parsing API responses), you can use the iron-enum-zod helper to create an IronEnum and a zod schema from a single definition.
npm install iron-enum-zod zodThis gives you a single, powerful factory with constructors and parsing methods.
import { z } from 'zod';
import { createZodEnum } from 'iron-enum-zod';
// 1. Define your payload schemas using Zod
const StatusPayloads = {
  Loading: z.undefined(),
  Ready: z.object({ finishedAt: z.date() }),
  Error: z.object({ message: z.string(), code: z.number() }),
};
// 2. Create the enhanced enum factory
const Status = createZodEnum(StatusPayloads);
// 3. You get all the standard constructors
const ready = Status.self.Ready({ finishedAt: new Date() });
// 4. And you get new, type-safe parsing methods
const apiInput = { tag: "Ready", data: { finishedAt: "2025-10-25T10:00:00.000Z" } } as const;
// .parse() returns an enum that's been recursively parsed by zod then converted into an `IronEnum` type.
const apiParsed = Status.parse(apiInput);
// now use apiPased as a normal enum
apiParsed.match(...)
apiParsed.if(...)
// You can also access the raw schema
const UserSchema = z.object({
  id: z.string(),
  status: Status.schema,
});iron-enum-vue provides a createEnumMatch function that generates a component that uses slots for an idiomatic, type-safe matching experience.
<script setup lang="ts">
import { IronEnum } from "iron-enum";
import { createEnumMatch } from "iron-enum-vue";
import { ref } from "vue";
const Status = IronEnum<{
  Loading: undefined;
  Ready: { finishedAt: Date };
  Error: { message: string; code: number };
}>();
const EnumMatch = createEnumMatch(Status);
const statusValue = ref(Status.Loading());
</script>
<template>
  <EnumMatch :of="statusValue">
    <template #Loading>
      <div>Loading</div>
    </template>
    <template #Ready="{ finishedAt }">
      <div>Done at {{ finishedAt.toLocaleTimeString() }}</div>
    </template>
    <template #Error="{ message, code }">
      <div>Failed: {{ message }} ({{ code }})</div>
    </template>
    <!-- Optional fallback for any unhandled tag -->
    <template #_="{ tag }">
      <div>Unknown: {{ tag }}</div>
    </template>
  </EnumMatch>
</template>
const RemoteData = IronEnum<{
  NotAsked: undefined;
  Loading: undefined;
  Success: { data: any };
  Failure: { error: Error };
}>();
const state = RemoteData.Success({ data: { id: 1, name: "Item" } });
const processed = await state.matchAsync({
  NotAsked: async () => null,
  Loading: async () => "Loading...",
  Success: async ({ data }) => {
    // Async processing
    // const enhanced = await enhanceData(data);
    // return enhanced;
    return data; // Example
  },
  Failure: async ({ error }) => {
    // await logError(error);
    return null;
  }
});Enums have a built-in toJSON() method for easy serialization. Use _.parse() for deserialization from plain objects.
const Status = IronEnum<{
  Active: { since: string };
  Inactive: { reason: string };
}>();
const status = Status.Active({ since: new Date().toISOString() });
// Convert to JSON
const json = status.toJSON(); 
// { tag: "Active", data: { since: "2025-10-24T..." } }
// ... send over network ...
// Parse from JSON
const parsed = Status._.parse(json); 
console.log(parsed.tag); // "Active"const Message = IronEnum<{
  Text: { content: string };
  Image: { url: string; alt?: string };
  Video: { url: string; duration: number };
}>();
// Use `typeof Message._.typeOf` for the union type
function processMessage(msg: typeof Message._.typeOf) {
  // The tag property enables type narrowing
  switch (msg.tag) {
    case "Text":
      console.log(msg.data.content); // TypeScript knows this is string
      break;
    case "Image":
      console.log(msg.data.url);     // TypeScript knows this is a string
      break;
    case "Video":
      console.log(msg.data.duration); // TypeScript knows this is number
      break;
  }
}Normally each time you call IronEnum() a proxy is created, however this can be bypassed for performance-critical applications by providing the variant keys as parameters.
Passing in keys also adds key validation to the myEnum._.parse(...) method.
// Pre-allocated version (no Proxy)
const Status = IronEnum<{
  Idle: undefined;
  Running: { pid: number };
  Stopped: { exitCode: number };
}>({ 
  keys: ["Idle", "Running", "Stopped"] // <- provide all keys in an array
});
// This avoids the Proxy overhead for better performanceEvery enum instance has these methods:
tag: The variant name (discriminant).data: The variant's associated data.toJSON(): Convert to plain object.is(key): Conditional check for if(..) statements.if(key, onMatch?, onMismatch?): Conditional execution.ifNot(key, onMismatch?, onMismatch?): Inverse conditional.match(handlers): Optional exhaustive pattern matching, fallback allowedmatchAsync(handlers): Async pattern matching.matchExhaustive(handlers)Exhaustive pattern matching, no fallback method allowed.
In addition to enum methods:
isOk(): Check if Result is Ok.isErr(): Check if Result is Err.unwrap(): Get value or throw error.unwrap_or(default): Get value or return default.unwrap_or_else(fn): Get value or compute default.ok(): Convert toOption, discarding error.
In addition to enum methods:
isSome(): Check if Option has a value.isNone(): Check if Option is None.unwrap(): Get value or throw error.unwrap_or(default): Get value or return default.unwrap_or_else(fn): Get value or compute default.ok_or(error): Convert toResultwith provided error.ok_or_else(fn): Convert toResultwith computed error.
- Use exhaustive matching - Always handle all variants or use 
_fallback. - Leverage type inference - Let TypeScript infer types from your variants.
 - Prefer Option/Result - Use built-in types for common patterns.
 - Keep payloads immutable - Treat enum data as read-only.
 - Use meaningful variant names - Make your code self-documenting.
 
const State = IronEnum<{
  Idle: undefined;
  Processing: { taskId: string; startedAt: Date };
  Completed: { taskId: string; result: string };
  Failed: { taskId: string; error: Error };
}>();
class TaskProcessor {
  private state = State.Idle();
  
  start(taskId: string) {
    this.state = State.Processing({ taskId, startedAt: new Date() });
  }
  
  complete(result: string) {
    this.state.if("Processing", ({ taskId }) => {
      this.state = State.Completed({ taskId, result });
    });
  }
  
  getStatus(): string {
    return this.state.match({
      Idle: () => "Ready",
      Processing: ({ taskId }) => `Processing ${taskId}...`,
      Completed: ({ taskId }) => `Task ${taskId} completed`,
      Failed: ({ error }) => `Failed: ${error.message}`
    });
  }
}const ValidationResult = IronEnum<{
  Valid: { value: string };
  Invalid: { errors: string[] };
}>();
function validateEmail(email: string): ValidationResult {
  const errors: string[] = [];
  
  if (!email) errors.push("Email is required");
  if (!email.includes("@")) errors.push("Invalid email format");
  if (email.length > 100) errors.push("Email too long");
  
  return errors.length > 0 
    ? ValidationResult.Invalid({ errors })
    : ValidationResult.Valid({ value: email.toLowerCase() });
}
// Usage
const result = validateEmail("user@example.com");
result.match({
  Valid: ({ value }) => console.log("Email accepted:", value),
  Invalid: ({ errors }) => console.error("Validation failed:", errors)
});MIT © 2025 Scott Lott
Made with ❤️ by a developer who misses Rust's enums in TypeScript