iron-enum
Version:
Rust like enums for Typescript
428 lines • 14.3 kB
JavaScript
;
/**
* # IronEnum
* A **zero‑dependency** helper that brings *Rust‑like tagged unions* (aka algebraic data
* types) to TypeScript **while staying 100 % runtime‑light**. You get:
*
* * **Ergonomic, type‑safe constructors** – payload gets the right shape or you don’t compile.
* * **Pattern‑matching helpers** (`match`, `matchAsync`).
* * **Fluent guards** (`if.*`, `ifNot.*`).
* * **Rust‑inspired convenience wrappers** – `Option`, `Result`, `Try`, `TryInto`.
* * **Smart payload rules** – when *every* property of the payload object is optional the
* argument itself becomes optional, so `Enum.Variant()` and `Enum.Variant({})` are both
* legal.
*
* ```ts
* const Status = IronEnum<{
* Loading: undefined
* Ready: { finishedAt: Date }
* }>();
*
* const a = Status.Loading(); // no payload
* const b = Status.Ready({ finishedAt: new Date() });
*
* console.log(a.match({
* Loading: () => "still working…",
* Ready: ({ finishedAt }) => `done at ${finishedAt}`,
* }));
* ```
*
* ## Table of contents (public surface)
* 1. `IronEnum` – generic builder
* 2. `EnumFactory` / `EnumMethods` – runtime value API (guards + matchers)
* 3. `Option`, `Some`, `None` – maybe‑value helpers
* 4. `Result`, `Ok`, `Err` – success/error helpers
* 5. `Try`, `TryInto` – convert imperative `throw`s into functional `Result`s
*
* ---------------------------------------------------------------------------
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TryInto = exports.Try = exports.None = exports.Some = exports.Option = exports.Err = exports.Ok = exports.Result = void 0;
exports.IronEnum = IronEnum;
/**
* Creates a single variant object internally, with all the associated utility methods.
*
* @param allVariants - The entire variants record for reference (not used directly, but typed).
* @param tag - The variant key being constructed.
* @param data - The associated data for this variant key.
* @returns An object with `tag`, `data`, and all the utility methods (`if`, `ifNot`, `match`, etc.).
*/
function enumFactory(allVariants, tag, data) {
if (tag === "_") {
throw new Error('Variant key "_" is reserved; cannot use "_" as a variant name.');
}
return {
tag,
data: data,
payload: data,
toJSON: () => ({ [tag]: data }),
key: () => tag,
if: new Proxy({}, {
get: (_tgt, prop) => {
return (callback, elseCallback) => {
if (prop === tag) {
if (callback) {
const result = callback(data);
return result === undefined ? true : result;
}
return true;
}
else if (elseCallback) {
const result = elseCallback({ [tag]: data });
return result === undefined ? false : result;
}
return false;
};
}
}),
ifNot: new Proxy({}, {
get: (_tgt, prop) => {
return (callback, elseCallback) => {
if (prop !== tag) {
if (callback) {
const result = callback({ [tag]: data });
return result === undefined ? true : result;
}
return true;
}
else if (elseCallback) {
const result = elseCallback({ [tag]: data });
return result === undefined ? false : result;
}
return false;
};
}
}),
match: (callbacks) => {
const maybeFn = callbacks[tag];
if (maybeFn) {
return maybeFn(data);
}
const catchAll = callbacks._;
if (catchAll) {
return catchAll();
}
throw new Error(`No handler for variant "${String(tag)}" and no "_" fallback`);
},
matchAsync: async (callbacks) => {
const maybeFn = callbacks[tag];
if (maybeFn) {
return await maybeFn(data);
}
const catchAll = callbacks._;
if (catchAll) {
return await catchAll();
}
throw new Error(`No handler for variant "${String(tag)}" and no "_" fallback`);
}
};
}
/**
* Constructs an enum "builder" object.
*
* Each key in the `ALL` record becomes a function to produce that variant value.
*
* Usage Example:
* ```ts
* const MyEnum = IronEnum<{ Foo: string, Bar: number }>();
* const fooValue = MyEnum.Foo("some text");
* const barValue = MyEnum.Bar(123);
*
* ```
*
* Note: Do not use the "_" key in your variants — it is reserved for catch-all logic.
*/
function IronEnum() {
// Using a Proxy to dynamically handle variant construction
// and the special "parse" method at runtime.
return new Proxy({}, {
get: (_tgt, prop) => {
if (prop === "_") {
return new Proxy({}, {
get: (_tgt2, prop2) => {
if (prop2 == "parse") {
return (dataObj) => {
const keys = Object.keys(dataObj);
if (keys.length !== 1) {
throw new Error(`Expected exactly 1 variant key, got ${keys.length}`);
}
const actualKey = keys[0];
return enumFactory({}, actualKey, dataObj[actualKey]);
};
}
throw new Error(`Property '${prop2}' not availalbe at runtime!`);
}
});
}
return (...args) => {
// if caller omitted the argument *and* the payload type is an object,
// inject an empty object so runtime matches the static type
const data = args.length === 0 ? {} : args[0];
return enumFactory({}, prop, data);
};
}
});
}
/**
* A Result type constructor for a success or error scenario, similar to Rust's `Result<T, E>`.
*
* Example usage:
* ```ts
* const NumResult = Result<number, string>();
* const okVal = NumResult.Ok(42);
* console.log(okVal.unwrap()); // 42
*
* const errVal = NumResult.Err("something happened");
* console.log(errVal.unwrap_or(0)); // 0
* ```
*/
const Result = () => (() => {
const resultEnum = IronEnum();
return {
_: resultEnum._,
Ok: (value) => ({
...resultEnum.Ok(value),
unwrap: () => value,
unwrap_or: x => value,
isOk: () => true,
isErr: () => false,
unwrap_or_else: x => value,
ok: () => (0, exports.Option)().Some(value)
}),
Err: (value) => ({
...resultEnum.Err(value),
unwrap: () => {
if (value instanceof Error) {
throw value;
}
else if (typeof value == "string") {
throw new Error(value);
}
else if (typeof value !== "undefined" && value !== null && typeof value.toString == "function") {
throw new Error(value.toString());
}
else {
throw new Error(`Called .unwrap() on an Result.Err enum!`);
}
},
isOk: () => false,
isErr: () => true,
unwrap_or: x => x,
unwrap_or_else: x => x(),
ok: () => (0, exports.Option)().None()
})
};
})();
exports.Result = Result;
/**
* A convenience function to build a `Result.Ok` variant inline, useful when you only care about a single Ok type.
*
* ```ts
* const val = Ok(123);
* console.log(val.unwrap()); // 123
* ```
*/
const Ok = (value) => (0, exports.Result)().Ok(value);
exports.Ok = Ok;
/**
* A convenience function to build a `Result.Err` variant inline, useful when you only care about a single Err type.
*
* ```ts
* const errorVal = Err("something went wrong");
* console.log(errorVal.match({
* Ok: (v) => `Got OK with value ${v}`,
* Err: (e) => `Got Err: ${e}`
* }));
* ```
*/
const Err = (error) => (0, exports.Result)().Err(error);
exports.Err = Err;
/**
* An Option type constructor for a possibly-undefined value, similar to Rust's `Option<T>`.
*
* Example usage:
* ```ts
* const NumOption = Option<number>();
*
* const someVal = NumOption.Some(123);
* console.log(someVal.unwrap()); // 123
*
* const noneVal = NumOption.None();
* console.log(noneVal.unwrap_or(0)); // 0
* ```
*/
const Option = () => (() => {
const optEnum = IronEnum();
return {
_: optEnum._,
Some: (value) => ({
...optEnum.Some(value),
isSome: () => true,
isNone: () => false,
unwrap: () => value,
unwrap_or: x => value,
unwrap_or_else: x => value,
ok_or: (err) => (0, exports.Result)().Ok(value),
ok_or_else: (err) => (0, exports.Result)().Ok(value),
_try: () => null
}),
None: () => ({
...optEnum.None(),
isSome: () => false,
isNone: () => true,
unwrap: () => {
throw new Error(`Called .unwrap() on an Option.None enum!`);
},
unwrap_or: x => x,
unwrap_or_else: x => x(),
ok_or: (err) => (0, exports.Result)().Err(err),
ok_or_else: (err) => (0, exports.Result)().Err(err()),
_try: () => null
})
};
})();
exports.Option = Option;
/**
* A convenience function to construct `Option.Some` inline with a given value.
*
* ```ts
* const s = Some("Hello");
* console.log(s.unwrap()); // "Hello"
* ```
*/
const Some = (value) => (0, exports.Option)().Some(value);
exports.Some = Some;
/**
* A convenience function to construct `Option.None` inline with no associated data.
*
* ```ts
* const n = None();
* console.log(n.unwrap_or("default")); // "default"
* ```
*/
const None = () => (0, exports.Option)().None();
exports.None = None;
/**
* A utility for wrapping function calls (both synchronous and asynchronous)
* in a `Result` type, capturing successful outputs or caught exceptions.
*
* This helps simplify error handling and improves readability by avoiding
* `try/catch` blocks scattered throughout your code.
*
* ## Example (Sync):
* ```ts
* const result = Try.sync(() => riskyOperation());
* if (result.if.Ok()) {
* console.log("Success:", result.value);
* } else {
* console.error("Error:", result.error);
* }
* ```
*
* ## Example (Async):
* ```ts
* const result = await Try.async(() => fetchData());
* if (result.if.Ok()) {
* console.log("Fetched:", result.value);
* } else {
* console.error("Fetch failed:", result.error);
* }
* ```
*
* @property sync - Wraps a synchronous function and returns a `ResultFactory`
* with `Ok` or `Err` depending on whether an exception is thrown.
*
* @property async - Wraps an asynchronous function (returning a Promise) and
* returns a `Promise<ResultFactory>` containing the result or error.
*/
exports.Try = {
sync: (callback) => {
try {
const output = callback();
return (0, exports.Result)().Ok(output);
}
catch (e) {
return (0, exports.Result)().Err(e);
}
},
async: async (callback) => {
try {
const output = await callback();
return (0, exports.Result)().Ok(output);
}
catch (e) {
return (0, exports.Result)().Err(e);
}
}
};
/**
* A higher-order utility that converts a function (sync or async) into one that
* returns a `Result` instead of throwing exceptions. This is especially useful
* when you want to consistently return a safe result wrapper from potentially
* failing operations, while preserving the original function signature.
*
* This differs from `Try` by returning a **new function** rather than calling
* the function immediately.
*
* ## Example (Sync):
* ```ts
* const parseIntSafe = TryInto.sync((str: string) => {
* const num = parseInt(str);
* if (isNaN(num)) throw new Error("Invalid number");
* return num;
* });
*
* const result = parseIntSafe("42");
* if (result.if.Ok()) {
* console.log(result.value);
* } else {
* console.error(result.error)
* }
* ```
*
* ## Example (Async):
* ```ts
* const fetchSafe = TryInto.async(async (url: string) => {
* const res = await fetch(url);
* if (!res.ok) throw new Error("Bad response");
* return res.json();
* });
*
* const result = await fetchSafe("/api/data");
* if (result.if.Ok()) {
* console.log(result.value);
* } else {
* console.log(result.error);
* }
* ```
*
* @property sync - Wraps a synchronous function and returns a new function
* that returns a `Result` instead of throwing.
* @property async - Wraps an asynchronous function and returns a new function
* that returns a `Promise<Result>` instead of rejecting.
*/
exports.TryInto = {
sync: (callback) => {
return (...args) => {
try {
const output = callback(...args);
return (0, exports.Result)().Ok(output);
}
catch (e) {
return (0, exports.Result)().Err(e);
}
};
},
async: (callback) => {
return async (...args) => {
try {
const output = await callback(...args);
return (0, exports.Result)().Ok(output);
}
catch (e) {
return (0, exports.Result)().Err(e);
}
};
}
};
//# sourceMappingURL=mod.js.map