Super‑lightweight Rust‑style tagged unions for TypeScript — fully type‑safe, zero‑dependency, < 1 kB min+gz.
TL;DR Stop writing brittle
switch
statements or sprawlingif / else
chains. Model your program’s states with expressive, type‑sound enums that compile down to plain JavaScript objects with helper methods — no classes, no runtime bloat.
- Iron Enum
- Table of Contents
- Why Iron Enum?
- Installation
- Quick Start
- Pattern Matching & Guards
- < 8000 a href="#asyncworkflows">Async Workflows
- Option & Result Helpers
- Try / TryInto Utilities
- Advanced Recipes
- FAQ & Trade‑offs
- Contributing
- License
- Keywords
- Clarity — Express all possible states in one place; TypeScript warns you when you forget a branch.
- Maintainability — Adding a new variant instantly surfaces every site that needs to handle it.
- Functional Flair — Great for FP‑oriented codebases or anywhere you want to banish
null
& friends. - Safe Data Transport —
toJSON()
/_.parse()
make it effortless to serialize across the wire.
Native discriminated unions are great, but they leave you to hand‑roll guards and pattern matching every time. Iron Enum wraps the same type‑level guarantees in an ergonomic, reusable runtime API.
npm i iron-enum
# or
pnpm add iron-enum
# or
yarn add iron-enum
import { IronEnum } from "iron-enum";
// 1. Declare your variants
const Status = IronEnum<{
Idle: undefined;
Loading: undefined;
Done: { items: number };
}>();
// 2. Produce values
const state = Status.Done({ items: 3 });
// 3. Handle them exhaustively
state.match({
Idle: () => console.log("No work yet."),
Loading: () => console.log("Crunching…"),
Done: ({ items }) => console.log(`Completed with ${items} items.`),
});
// 4. Handle as args
const handleLoadingState = (stateInstance: typeof Status._.typeOf) => { /* .. */ }
handleLoadingState(state);
// branching
value.match({
Foo: (x) => doSomething(x),
Bar: (s) => console.log(s),
_: () => fallback(), // optional catch‑all
});
// return with type inference
const returnValue = value.match({
Foo: (x) => x,
Bar: (s) => s,
_: () => null
});
// typeof returnValue == x | s | null
// branching
value.if.Foo(
({ count }) => console.log(`It *is* Foo with ${count}`),
() => console.log("It is NOT Foo"),
);
// return through callbacks with type inference
const isNumber = value.if.Foo(
// if true
({ count }) => count,
// if false
() => 0,
);
// in statement, callbacks optional
if (value.if.Foo()) {
// value is Foo!
} else {
// value is NOT Foo!
}
Both helpers return the callback’s result or a boolean when you omit callbacks, so they slot neatly into expressions.
Need to await network calls inside branches? Use matchAsync
:
await status.matchAsync({
Idle: async () => cache.get(),
Loading: async () => await poll(),
Done: async ({ items }) => items,
});
import { Option, Result } from "iron-enum";
// Option<T>
const MaybeNum = Option<number>();
const some = MaybeNum.Some(42);
const none = MaybeNum.None();
console.log(some.unwrap()); // 42
console.log(none.unwrap_or(0)); // 0
// Result<T, E>
const NumOrErr = Result<number, Error>();
const ok = NumOrErr.Ok(123);
const err = NumOrErr.Err(new Error("Boom"));
ok.match({ Ok: (v) => v, Err: console.error });
The helper instances expose Rust‑style sugar (isOk()
, isErr()
, ok()
, etc.) while still being regular Iron Enum variants under the hood.
Run any operation that may throw and return it as a Result
type:
import { Try } from "iron-enum";
const result = Try.sync(() => {
// risk stuffy that might throw new Error()
});
if (result.if.Ok()) { /* … */ }
Or create a new function that may throw that always returns a Result
.
import { TryInto } from "iron-enum";
const safeParseInt = TryInto.sync((s: string) => {
const n = parseInt(s, 10);
if (Number.isNaN(n)) throw new Error("NaN");
return n;
});
const result = safeParseInt("55");
result.if.Ok((value) => {
console.log(value) // 55;
})
Try
and TryInto
also have async variants that work with Promises
and async/await
.
- Nested Enums — compose enums inside payloads for complex state machines.
- Optional‑object payloads — if all payload keys are optional, the constructor arg becomes optional:
E.Query()
==E.Query({})
. - Serialization —
enum.toJSON()
➜{ Variant: payload }, and
Enum._.parse(obj)
brings it back. - Type Extraction —
typeof MyEnum._.typeOf
gives you the union type of all variants.
Does Iron Enum add runtime overhead?
No. Each constructed value is a plain object { tag, data, …helpers }
. The helper methods are closures created once per value; for most apps this is negligible compared with the clarity you gain.
Why not stick with vanilla TypeScript unions?
Vanilla unions keep types safe but leave guards up to you. Iron Enum bakes common guard logic into reusable helpers and ensures your match statements stay exhaustive.
Can I tree‑shake out helpers I don’t use?
Yes. Because everything is property‑based access on the enum instance, dead‑code elimination removes unused helpers in modern bundlers.
PRs and issues are welcome!
MIT © Scott Lott
typescript, enum, tagged union, tagged unions, discriminated union, algebraic data type, adt, sum type, union types, rust enums, rust, pattern matching, option type, result type, functional programming