Iron Enum is a lightweight library that brings Rust like powerful, type-safe, runtime "tagged enums" (also known as "algebraic data types" or "discriminated unions") to TypeScript. It provides a fluent, functional style for creating, inspecting, and pattern-matching on variant data structures — without needing complex pattern-matching libraries or large frameworks.
- Lightweight and Zero-Dependencies: A minimal implementation (only 400 bytes!) that relies solely on TypeScript’s advanced type system and the ES6
Proxy
object. - Type-Safe Tagged Variants: Each variant is created with a unique tag and associated data, which TypeScript can strongly type-check at compile time.
- Pattern Matching: Convenient
match
andmatchAsync
methods allow you to handle each variant in a type-safe manner, including a default_
fallback. - Conditional Checks: Intuitive
if
andifNot
methods let you easily check which variant you’re dealing with and optionally run callbacks. - Supports "Empty" Variants: Variants can be defined without associated data (using
undefined
), making it easy to represent states like "None" or "Empty." - Reduced Boilerplate: No need to write large switch statements or manually check discriminant fields—simply define variants and let the library handle the rest.
- Improved Code Clarity: Instead of multiple conditionals scattered across your code, use straightforward pattern matching to handle each variant in one place.
- Type Safety: TypeScript ensures that when you match a variant, you receive the correct data type automatically. No more manual type guards!
- Maintainability: Changes to variant definitions are easy to propagate. Add or remove variants in one place and let the type system guide necessary changes in your code.
- Functional Programming Style: Ideal for FP-oriented codebases or whenever you want to avoid
class
-based inheritance and complex hierarchies. - Better User Experience: Faster onboarding for new team members. The tagged enum pattern is intuitive and self-documenting.
Suppose you want an enum-like type with three variants:
- Foo contains an object with an x field.
- Bar contains a string.
- Empty contains no data.
You can define these variants as follows:
import { IronEnum } from "iron-enum";
type MyVariants = {
Foo: { x: number };
Bar: string;
Empty: undefined;
};
const MyEnum = IronEnum<MyVariants>();
To create values, simply call the variant functions:
const fooValue = MyEnum.Foo({ x: 42 });
const barValue = MyEnum.Bar("Hello");
const emptyValue = MyEnum.Empty();
Each call returns a tagged object with methods to inspect or match the variant.
Use match to handle each variant:
fooValue.match({
Foo: (val) => console.log("Foo with:", val), // val is { x: number }
Bar: (val) => console.log("Bar with:", val),
Empty: () => console.log("It's empty"),
_: () => console.log("No match") // Optional fallback
});
The appropriate function is called based on the variant’s tag. If none match, _ is used as a fallback if provided.
You can quickly check the current variant with if and ifNot:
// If the variant is Foo, log a message and return true; otherwise return false.
const isFoo: boolean = fooValue.if.Foo((val) => {
console.log("Yes, it's Foo with x =", val.x);
});
// If the variant is not Bar, log a message; if it is Bar, return false.
const notBar: boolean = fooValue.ifNot.Bar(() => {
console.log("This is definitely not Bar!");
});
// Values can be returned through the function and will be inferred.
// The type for `notBar` will be inferred as "boolean | string".
const notBar = fooValue.ifNot.Bar(() => {
console.log("This is definitely not Bar!");
return "not bar for sure";
});
// Callback is optional, useful for if statements
if(fooValue.ifNot.Bar()) {
// not bar
}
Both methods return a boolean or the callback result, making conditional logic concise and expressive.
When dealing with asynchronous callbacks, use matchAsync:
const matchedResult = await barValue.matchAsync({
Foo: async (val) => { /* ... */ },
Bar: async (val) => {
const result = await fetchSomeData(val);
return result;
},
Empty: async () => {
await doSomethingAsync();
},
_: async () => "default value"
});
console.log("Async match result:", matchedResult);
The matchAsync method returns a Promise that resolves with the callback’s return value.
In the event that you need to access the data directly, you can use the unwrap method.
const simpleEnum = IronEnum<{
foo: { text: string },
bar: { title: string }
}>();
const testValue = simpleEnum.foo({text: "hello"});
// typeof unwrapped = ["foo", {text: string}] | ["bar", {title: string}]
const unwrapped = testValue.unwrap();
The library contains straightforward implmentations of Rust's Option
and Result
types.
import { Option, Result } from "iron-enum";
const myResult = Result<{Ok: boolean, Err: string}>();
const ok = myResult.Ok(true);
ok.match({
Ok: (value) => {
console.log(value) // true;
},
_: () => { /* .. */ }
});
const myOption = Option<number>();
const optNum = myOption.Some(22);
optNum.match({
Some: (val) => {
console.log(val) // 22;
},
None: () => { /* .. */ }
})
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.