UNPKG

iron-enum

Version:

Rust like enums for Typescript

595 lines (447 loc) 15.7 kB
# 🦾 Iron Enum Super‑lightweight **Rust‑style tagged unions for TypeScript** — fully type‑safe, zero‑dependency, ~1 kB min+gz. [![GitHub Repo stars](https://img.shields.io/github/stars/only-cliches/iron-enum)](https://github.com/only-cliches/iron-enum) [![NPM Version](https://img.shields.io/npm/v/iron-enum)](https://www.npmjs.com/package/iron-enum) [![JSR Version](https://img.shields.io/jsr/v/%40onlycliches/iron-enum)](https://jsr.io/@onlycliches/iron-enum) [![npm package minimized gzipped size](https://badgen.net/bundlephobia/minzip/iron-enum)](https://bundlephobia.com/package/iron-enum@latest) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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*