Iron Enum is a lightweight library that brings Rust-like tagged enums (also called algebraic data types or discriminated unions) to TypeScript. It provides a fluent, functional style for creating, inspecting, and pattern-matching on variant data structures — all with TypeScript’s strong type-checking at compile time.
Try Iron Enum Now - Free StackBlitz Sandbox
- Iron Enum
- Lightweight & Zero Dependencies: A minimal, dependency-free implementation (~1k bytes) leveraging TypeScript’s advanced type system and Proxies.
- Type-Safe Tagged Variants: Each variant is created with a unique tag and associated data, enforced at compile time by TypeScript.
- Powerful Pattern Matching: Intuitive
match
/matchAsync
methods eliminate the need for brittle switch statements or nested conditionals. - Conditional Checks: Minimal overhead for checking variants (via
if
/ifNot
) in a readable style. - Supports "Empty" Variants: Easily represent states like
None
orEmpty
by defining a variant withundefined
. - Reduced Boilerplate: The library handles the “discriminant” under the hood, cutting down on repetitive code.
- Improved Code Clarity: Handle your logic in one place using pattern matching, reducing scattered if-else blocks and clarifying your app’s possible states.
- Type Safety: No more unverified type casts. TypeScript ensures that once you branch on a variant, you get the correct data type without extra checks.
- Maintainability & Scalability: Adding or removing variants is simple—update your variant definitions, and TypeScript will highlight where changes are needed.
- Functional Style: Iron Enum fits perfectly in FP-oriented codebases, or wherever you want to avoid
class
-based hierarchies and large frameworks. - Great for Error Handling & State Management: Pattern matching simplifies code that handles many possible outcomes.
Install via npm or yarn:
npm install iron-enum
# or
yarn add iron-enum
Then import and start defining your enums:
import { IronEnum } from "iron-enum";
Suppose you want an enum-like type with three variants:
- Foo contains an object
{ x: number }
- Bar contains a string
- Empty contains no data (i.e.,
undefined
)
import { IronEnum } from "iron-enum";
// 1. Define a record of variant keys to their associated data types
type MyVariants = {
Foo: { x: number };
Bar: string;
Empty: undefined;
};
// 2. Construct an enum builder using IronEnum
const MyEnum = IronEnum<MyVariants>();
You can create new enum values by calling the builder’s variant methods:
const fooValue = MyEnum.Foo({ x: 42 });
const barValue = MyEnum.Bar("Hello");
const emptyValue = MyEnum.Empty();
Each call returns a tagged object with helpful methods (e.g., match
, if
, ifNot
, etc.).
Handle each variant cleanly with match
. You can also provide a fallback (_
) if you wish:
fooValue.match({
Foo: (val) => {
// val is { x: number }
console.log("Foo with:", val.x);
},
Bar: (val) => {
console.log("Bar with:", val);
},
Empty: () => {
console.log("It's empty!");
},
_: () => {
// Optional fallback if you didn't specify all variants
console.log("No match found!");
}
});
if
and ifNot
let you quickly test if a variant matches (or doesn’t match) a specific key, and optionally run callbacks.
// 1. Returns true/false by default
const isFoo = fooValue.if.Foo();
// isFoo === true if fooValue.tag === 'Foo', otherwise false
// 2. Optional callbacks
fooValue.if.Foo(
(val) => { // called if enum is "Foo" variant.
console.log("Yes, it's Foo:", val.x);
return "someReturnValue";
}, // else:
(json) => {
// json is { Bar?: string, Empty?: undefined, Foo?: { x: number } }
return "elseReturnValue";
}
);
// The return value is inferred. If you provide returns, you'll get that union type back.
fooValue.ifNot.Bar(() => {
console.log("Definitely not a Bar variant!");
});
When working with async logic, use matchAsync
. Each branch callback should return a Promise
(or use async
):
const result = await barValue.matchAsync({
Foo: async (val) => {
// Handle Foo asynchronously
return await fetchSomeData(val);
},
Bar: async (val) => {
// Handle Bar asynchronously
return "barValue resolved";
},
Empty: async () => {
await doSomethingAsync();
return "Handled Empty";
},
_: async () => "Default fallback"
});
// return type is inferred from match arm functions
console.log("Async match result:", result);
Iron Enum values can be easily serialized to JSON (or sent across the network) by using toJSON()
. To get them back into an Iron Enum value, you can call parse
on the builder’s _
property.
const simpleEnum = IronEnum<{
foo: { text: string };
bar: { title: string };
}>();
const originalValue = simpleEnum.foo({ text: "hello" });
const jsonValue = originalValue.toJSON();
// jsonValue is now { foo: { text: "hello" } }
const parsedValue = simpleEnum._.parse(jsonValue);
// parsedValue is again a fully featured Iron Enum variant
parsedValue.match({
foo: (val) => console.log("Parsed back successfully:", val),
_: () => {}
});
Iron Enum variants can contain anything: classes, nested objects, arrays, or even other Iron Enums.
class SimpleClass {
constructor(public name: string) {}
}
const nestedEnum = IronEnum<{ alpha: number; beta: string }>();
const complexEnum = IronEnum<{
test: typeof nestedEnum._.typeOf;
aClass: SimpleClass;
nestedData: {
foo: string;
bar: string;
array: { someProperty: string; anotherProperty: number }[];
};
}>();
const myInstance = complexEnum.aClass(new SimpleClass("TestName"));
const myNested = complexEnum.test(nestedEnum.alpha(42));
One of the biggest perks is that you can pass these enums around, and the type system will protect you from invalid usage:
const testEnum = IronEnum<{ foo: string; bar: string }>();
function handleTestEnum(value: typeof testEnum._.typeOf) {
// Now we can pattern-match safely
return value.match({
foo: (val) => `Got foo: ${val}`,
bar: (val) => `Got bar: ${val}`,
});
}
const result = handleTestEnum(testEnum.foo("Hello!"));
console.log(result); // "Got foo: Hello!"
TypeScript automatically infers the data type for each branch when you match on an Iron Enum. You can also create custom types to extract payload information:
const myEnum = IronEnum<{ Foo: { x: number }; Bar: string }>();
const fooValue = myEnum.Foo({ x: 42 });
type InferFooDataType<X extends typeof myEnum._.typeOf> =
X extends { tag: "Foo"; data: infer Payload }
? Payload
: never;
type Inferred = InferFooDataType<typeof fooValue>;
// Inferred = { x: number }
Iron Enum includes convenient implementations for two common patterns: Option
and Result
.
Absolutely! Here’s a set of clear and practical usage examples for the Option
and Result
types that you can include in your README to help users understand how to use your library effectively.
The Option
type is useful for representing values that may or may not exist.
const NumberOption = Option<number>();
const some = NumberOption.Some(42);
const none = NumberOption.None();
some.unwrap(); // 42
none.unwrap(); // ❌ throws: Called .unwrap() on an Option.None enum!
some.unwrap_or(100); // 42
none.unwrap_or(100); // 100
some.unwrap_or_else(() => 999); // 42
none.unwrap_or_else(() => 999); // 999
const OkOrErr = some.ok_or("Not found"); // Ok(42)
const ErrRes = none.ok_or("Not found"); // Err("Not found")
const OkOrErrLazy = some.ok_or_else(() => "fail"); // Ok(42)
const ErrResLazy = none.ok_or_else(() => "fail"); // Err("fail")
some.match({
Some: (val) => `Value is ${val}`,
None: () => "No value"
}); // "Value is 42"
none.match({
Some: (val) => `Value is ${val}`,
None: () => "No value"
}); // "No value"
The Result
type is useful for returning either a success value (Ok
) or an error (Err
).
const NumResult = Result<number, string>();
const ok = NumResult.Ok(123);
const err = NumResult.Err("Something went wrong");
ok.unwrap(); // 123
err.unwrap(); // ❌ throws: Called .unwrap() on a Result.Err enum!
ok.unwrap_or(0); // 123
err.unwrap_or(0); // 0
ok.unwrap_or_else(() => 999); // 123
err.unwrap_or_else(() => 999); // 999
const SomeOpt = ok.ok(); // Some(123)
const NoneOpt = err.ok(); // None()
ok.match({
Ok: (val) => `Success: ${val}`,
Err: (e) => `Failure: ${e}`
}); // "Success: 123"
err.match({
Ok: (val) => `Success: ${val}`,
Err: (e) => `Failure: ${e}`
}); // "Failure: Something went wrong"
Contributions, suggestions, and feedback are welcome! Please open an issue or submit a pull request on the GitHub repository.
This library is available under the MIT license. See the LICENSE file for details.