iron-enum
Version:
Rust like enums for Typescript
595 lines (447 loc) • 15.7 kB
Markdown
# 🦾 Iron Enum
Super‑lightweight **Rust‑style tagged unions for TypeScript** — fully type‑safe, zero‑dependency, ~1 kB min+gz.
[](https://github.com/only-cliches/iron-enum)
[](https://www.npmjs.com/package/iron-enum)
[](https://jsr.io/@onlycliches/iron-enum)
[](https://bundlephobia.com/package/iron-enum@latest)
[](https://www.typescriptlang.org/)
[](https://opensource.org/licenses/MIT)
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.
[▶ Open playground](https://stackblitz.com/edit/iron-enum-sandbox?file=src/main.ts)
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [Built-in Types](#built-in-types)
- [Result<T, E>](#resultt-e)
- [Option<T>](#optiont)
- [Try and TryInto](#try-and-tryinto)
- [EcoSystem Helpers](#ecosystem-helpers)
- [Zod](#zod)
- [Vue](#vue)
- [Advanced Usage](#advanced-usage)
- [Async Pattern Matching](#async-pattern-matching)
- [Serialization & Parsing](#serialization--parsing)
- [Type Guards](#type-guards-and-narrowing)
- [Performance](#performance-optimization)
- [API Reference](#api-reference)
- [Best Practices](#best-practices)
- [Examples](#examples)
## Features
- 🦀 **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 `match` and `matchAsync` methods
- 🛡️ **Error handling** - Built-in `Try` and `TryInto` utilities
## Installation
```bash
npm install iron-enum
# or
yarn add iron-enum
# or
pnpm add iron-enum
```
## Quick Start
```tsx
import { 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>
)
}
```
## Core Concepts
### Creating Enums
IronEnum uses TypeScript's type system to create discriminated unions with zero runtime overhead:
```ts
// 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()
});
```
### Pattern Matching
The `match` method ensures exhaustive handling of all variants:
```ts
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
});
```
### Guards and Conditionals
Use `if` and `ifNot` for conditional logic:
```ts
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")
);
```
## Built-in Types
### Result\<T, E\>
Rust-style error handling:
```ts
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)); // 5
```
### Option\<T\>
Nullable value handling:
```ts
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 | null
```
### Try and TryInto
Automatic exception handling:
```ts
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)
});
```
## EcoSystem Helpers
### Zod
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.
```bash
npm install iron-enum-zod zod
```
This gives you a single, powerful factory with constructors and parsing methods.
```ts
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,
});
```
---
### Vue
`iron-enum-vue` provides a `createEnumMatch` function that generates a component that uses slots for an idiomatic, type-safe matching experience.
```vue
<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>
```
## Advanced Usage
### Async Pattern Matching
```ts
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;
}
});
```
### Serialization & Parsing
Enums have a built-in `toJSON()` method for easy serialization. Use `_.parse()` for deserialization from plain objects.
```ts
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"
```
### Type Guards and Narrowing
```ts
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;
}
}
```
### Performance Optimization
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.
```ts
// 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 performance
```
## API Reference
### IronEnum Methods
Every 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 allowed
- **`matchAsync(handlers)`**: Async pattern matching.
- **`matchExhaustive(handlers)`** Exhaustive pattern matching, no fallback method allowed.
### Result Methods
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 to `Option`, discarding error.
### Option Methods
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 to `Result` with provided error.
- **`ok_or_else(fn)`**: Convert to `Result` with computed error.
## Best Practices
1. **Use exhaustive matching** - Always handle all variants or use `_` fallback.
2. **Leverage type inference** - Let TypeScript infer types from your variants.
3. **Prefer Option/Result** - Use built-in types for common patterns.
4. **Keep payloads immutable** - Treat enum data as read-only.
5. **Use meaningful variant names** - Make your code self-documenting.
## Examples
### State Machine
```ts
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}`
});
}
}
```
### Form Validation
```ts
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)
});
```
## License
MIT © 2025 Scott Lott
*Made with ❤️ by a developer who misses Rust's enums in TypeScript*