UNPKG

iron-enum

Version:

Rust like enums for Typescript

462 lines 13.8 kB
/** * IronEnum – Zero-dependency Rust-style tagged-union helpers for TypeScript. * * A small utility to build runtime representations of discriminated unions, * with type-safe pattern matching and ergonomic Result/Option types. * * @example * // Define an enum * const Status = IronEnum<{ * Loading: undefined; * Ready: { finishedAt: Date }; * Error: { message: string; code: number }; * }>(); * * // Create instances * const s1 = Status.Loading(); * const s2 = Status.Ready({ finishedAt: new Date() }); * * // Narrow using .is() * if (s2.is("Ready")) { * s2.data.finishedAt.toISOString(); * } * * // Match with fallback * const msg = s2.match({ * Loading: () => "Working", * Ready: ({ finishedAt }) => finishedAt.toISOString(), * _: () => "Unknown" * }); * * // Exhaustive match (no fallback allowed) * const iso = s2.matchExhaustive({ * Loading: () => "n/a", * Ready: ({ finishedAt }) => finishedAt.toISOString(), * Error: () => "n/a", * }); * * @example * // Result and Option usage * const R = Result<number, string>(); * const r1 = R.Ok(42); * const r2 = R.Err("nope"); * const val = r1.unwrap_or(0); // 42 * * const O = Option<number>(); * const some = O.Some(7); * const none = O.None(); * const out = some.map(x => x * 2).unwrap(); // 14 */ /* ============================================================================= * Factory Implementation * ============================================================================= */ /** * Create a concrete variant instance and attach all methods. * * This function centralizes the instance layout and avoids per-call * function allocation differences across factories. */ function enumFactory(_allVariants, tag, data, instance) { if (tag === "_") throw new Error("'_' is reserved as a fallback key."); // Self reference enables methods to pass a stable instance shape. const self = {}; const is = (key) => key === tag; const _if = (key, success, failure) => { const hit = key === tag; if (hit) { if (success) { const r = success(data, self); return (r === undefined ? true : r); } return true; } if (failure) { const r = failure(self); return (r === undefined ? false : r); } return false; }; const _ifNot = (key, success, failure) => { const miss = key !== tag; if (miss) { if (success) { const r = success(self); return (r === undefined ? true : r); } return true; } if (failure) { const r = failure(data, self); return (r === undefined ? false : r); } return false; }; const match = (callbacks) => { const specific = callbacks[tag]; const cb = specific ?? callbacks._; if (!cb) { throw new Error(`No handler for '${String(tag)}' and no '_' fallback`); } return specific ? cb(data, self) : cb(self); }; const matchAsync = async (callbacks) => { const specific = callbacks[tag]; const cb = specific ?? callbacks._; if (!cb) { throw new Error(`No handler for '${String(tag)}' and no '_' fallback`); } return specific ? cb(data, self) : cb(self); }; const matchExhaustive = (callbacks) => { const cb = callbacks[tag]; return cb(data, self); }; const selfProperties = { tag, data, instance, toJSON: () => ({ tag, data }), is: is, if: _if, ifNot: _ifNot, match: match, matchAsync: matchAsync, matchExhaustive: matchExhaustive, }; Object.assign(self, selfProperties); return self; } /** * Create a new enum factory. * * @param args - Optional arguments. * @param args.keys - An array of variant keys. Providing this skips the * Proxy-based implementation for a faster, pre-bound factory. * * @example * // Dynamic (Proxy-based, slower) * const Status = IronEnum<{ * A: undefined; * B: undefined; * }>(); * * // Pre-bound (Fast, no Proxy) * const FastStatus = IronEnum<{ * A: undefined; * B: undefined; * }>({ keys: ["A", "B"] }); */ export function IronEnum(args) { const keys = args?.keys; let result = {}; const parse = (dataObj) => { const actualKey = dataObj.tag; if (keys?.length && !keys.includes(actualKey)) { throw new Error(`Unexpected variant '${actualKey}'`); } return enumFactory({}, actualKey, dataObj.data, result); }; const _ = { typeTags: undefined, // [TYPE ONLY] typeData: undefined, // [TYPE ONLY] typeOf: undefined, // [TYPE ONLY] typeJson: undefined, // [TYPE ONLY] parse, fromJSON: parse, reviver(obj) { if (obj && typeof obj === "object" && "tag" in obj && "data" in obj) { // We use parse, but must cast the input. // The `in` checks are a reasonable safeguard. return parse(obj); } return obj; }, }; // Keyed fast-path (no Proxy) if (keys?.length) { result = { _ }; for (const key of keys) { result[key] = ((...args) => enumFactory({}, key, args[0], result)); } return result; } // Dynamic Proxy builder with built-in guard const BUILTINS = new Set(["toString", "valueOf", "inspect", "constructor"]); result = new Proxy({}, { get: (_tgt, prop) => { if (prop === "_") return _; if (typeof prop !== "string") return undefined; if (BUILTINS.has(prop)) { const fn = Object.prototype[prop]; return typeof fn === "function" ? fn.bind(result) : undefined; } return (...args) => { const data = args[0]; return enumFactory({}, prop, data, result); }; }, }); return result; } /** * Internal constructor for a typed Result factory. * * A fresh factory is produced per `<T,E>` instantiation to preserve * factory identity and allow future per-type runtime extensibility. */ const ResultInternal = () => { const R = IronEnum({ keys: ["Err", "Ok"] }); const OkCtor = (value) => { const base = R.Ok(value); return Object.assign(base, { unwrap: () => value, unwrap_or: () => value, unwrap_or_else: () => value, isOk: () => true, isErr: () => false, ok: () => Option().Some(value), map(f) { return Result().Ok(f(value)); }, mapErr(_f) { return Result().Ok(value); }, andThen(f) { return f(value); }, }); }; const ErrCtor = (error) => { const base = R.Err(error); return Object.assign(base, { unwrap: () => { throw error instanceof Error ? error : new Error(String(error ?? "Err")); }, unwrap_or: (x) => x, unwrap_or_else: (cb) => cb(), isOk: () => false, isErr: () => true, ok: () => Option().None(), map(_f) { return Result().Err(error); }, mapErr(f) { return Result().Err(f(error)); }, andThen(_f) { return Result().Err(error); }, }); }; return { _: R._, Ok: OkCtor, Err: ErrCtor }; }; /** * Create a typed Result factory `<T,E>`. * * @example * const StringResult = Result<string, Error>(); * const r1 = StringResult.Ok("hello"); * * function process(r: typeof StringResult._.typeOf) { * // ... * } */ export const Result = () => ResultInternal(); /** * Convenience Ok constructor for ad-hoc success values. * The `Err` type is `never`. * * @example * const r = Ok(123); // ResultVariant<{ Ok: number, Err: never }> */ export const Ok = (value) => Result().Ok(value); /** * Convenience Err constructor for ad-hoc error values. * The `Ok` type is `never`. * * @example * const r = Err("oops"); // ResultVariant<{ Ok: never, Err: string }> */ export const Err = (error) => Result().Err(error); /** * Internal constructor for a typed Option factory. */ const OptionInternal = () => { const O = IronEnum({ keys: ["None", "Some"] }); const SomeCtor = (value) => { const base = O.Some(value); return Object.assign(base, { isSome: () => true, isNone: () => false, unwrap: () => value, unwrap_or: () => value, unwrap_or_else: () => value, ok_or(_err) { return Result().Ok(value); }, ok_or_else(_errFn) { return Result().Ok(value); }, map(f) { return Option().Some(f(value)); }, andThen(f) { return f(value); }, filter(p) { return p(value) ? Option().Some(value) : Option().None(); }, }); }; const NoneCtor = () => { const base = O.None(); return Object.assign(base, { isSome: () => false, isNone: () => true, unwrap: () => { throw new Error("Called unwrap() on Option.None"); }, unwrap_or: (x) => x, unwrap_or_else: (cb) => cb(), ok_or(err) { return Result().Err(err); }, ok_or_else(errFn) { return Result().Err(errFn()); }, map(_f) { return Option().None(); }, andThen(_f) { return Option().None(); }, filter(_p) { return Option().None(); }, }); }; return { _: O._, Some: SomeCtor, None: NoneCtor }; }; /** * Create a typed Option factory `<T>`. * * @example * const NumberOption = Option<number>(); * const s = NumberOption.Some(100); * * function process(o: typeof NumberOption._.typeOf) { * // ... * } */ export const Option = () => OptionInternal(); /** * Convenience Some constructor for ad-hoc values. * * @example * const s = Some(123); // OptionVariant<{ Some: number, ... }> */ export const Some = (value) => Option().Some(value); /** * Convenience None constructor. * The `Some` type is `never`. * * @example * const n = None(); // OptionVariant<{ Some: never, ... }> */ export const None = () => Option().None(); /* ============================================================================= * Try / TryInto Utilities * ============================================================================= */ /** * Convert exception-throwing code into Result-returning code. */ export const Try = { /** * Execute a synchronous function and wrap the outcome. * * Ok on success, Err with the caught exception on failure. * * @example * const throws = () => { throw new Error("!"); }; * const r = Try.sync(throws); // Err(Error("!")) * * const r2 = Try.sync(() => JSON.parse("{")); // Err(SyntaxError(...)) * const r3 = Try.sync(() => JSON.parse('{"a":1}')); // Ok({ a: 1 }) */ sync(cb) { // Create a single factory for this function's return type const R = Result(); try { // Use the local factory's .Ok return R.Ok(cb()); } catch (e) { // Use the local factory's .Err return R.Err(e); } }, /** * Execute an asynchronous function and wrap the outcome. * * Resolves to Ok on success, Err with the caught exception on rejection. * * @example * const rejects = async () => { throw new Error("!"); }; * const r = await Try.async(rejects); // Err(Error("!")) * * const r2 = await Try.async(() => fetch("/good")); // Ok(Response) */ async async(cb) { // Create a single factory for this function's return type const R = Result(); try { // Use the local factory's .Ok return R.Ok(await cb()); } catch (e) { // Use the local factory's .Err return R.Err(e); } }, }; /** * Transform functions to return Result instead of throwing. */ export const TryInto = { /** * Wrap a synchronous function. The wrapper never throws. * * @example * const unsafeParse = (s: string) => JSON.parse(s); * const safeParse = TryInto.sync(unsafeParse); * * const r1 = safeParse('{"a":1}'); // Ok({ a: 1 }) * const r2 = safeParse("{"); // Err(SyntaxError(...)) */ sync(cb) { return (...args) => Try.sync(() => cb(...args)); }, /** * Wrap an asynchronous function. The wrapper resolves to Result * instead of rejecting. * * @example * const unsafeFetch = async (url: string) => { * const res = await fetch(url); * if (!res.ok) throw new Error(res.statusText); * return res.json(); * } * * const safeFetch = TryInto.async(unsafeFetch); * * const r1 = await safeFetch("/api/data"); // Ok(data) * const r2 = await safeFetch("/api/404"); // Err(Error("Not Found")) */ async(cb) { return async (...args) => Try.async(() => cb(...args)); }, }; //# sourceMappingURL=mod.js.map