iron-enum
Version:
Rust like enums for Typescript
472 lines (471 loc) • 14.4 kB
JavaScript
;
/**
* 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
*/
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;
/* =============================================================================
* 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"] });
*/
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: () => (0, exports.Option)().Some(value),
map(f) {
return (0, exports.Result)().Ok(f(value));
},
mapErr(_f) {
return (0, exports.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: () => (0, exports.Option)().None(),
map(_f) {
return (0, exports.Result)().Err(error);
},
mapErr(f) {
return (0, exports.Result)().Err(f(error));
},
andThen(_f) {
return (0, exports.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) {
* // ...
* }
*/
const Result = () => ResultInternal();
exports.Result = Result;
/**
* Convenience Ok constructor for ad-hoc success values.
* The `Err` type is `never`.
*
* @example
* const r = Ok(123); // ResultVariant<{ Ok: number, Err: never }>
*/
const Ok = (value) => (0, exports.Result)().Ok(value);
exports.Ok = Ok;
/**
* Convenience Err constructor for ad-hoc error values.
* The `Ok` type is `never`.
*
* @example
* const r = Err("oops"); // ResultVariant<{ Ok: never, Err: string }>
*/
const Err = (error) => (0, exports.Result)().Err(error);
exports.Err = Err;
/**
* 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 (0, exports.Result)().Ok(value);
},
ok_or_else(_errFn) {
return (0, exports.Result)().Ok(value);
},
map(f) {
return (0, exports.Option)().Some(f(value));
},
andThen(f) {
return f(value);
},
filter(p) {
return p(value) ? (0, exports.Option)().Some(value) : (0, exports.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 (0, exports.Result)().Err(err);
},
ok_or_else(errFn) {
return (0, exports.Result)().Err(errFn());
},
map(_f) {
return (0, exports.Option)().None();
},
andThen(_f) {
return (0, exports.Option)().None();
},
filter(_p) {
return (0, exports.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) {
* // ...
* }
*/
const Option = () => OptionInternal();
exports.Option = Option;
/**
* Convenience Some constructor for ad-hoc values.
*
* @example
* const s = Some(123); // OptionVariant<{ Some: number, ... }>
*/
const Some = (value) => (0, exports.Option)().Some(value);
exports.Some = Some;
/**
* Convenience None constructor.
* The `Some` type is `never`.
*
* @example
* const n = None(); // OptionVariant<{ Some: never, ... }>
*/
const None = () => (0, exports.Option)().None();
exports.None = None;
/* =============================================================================
* Try / TryInto Utilities
* ============================================================================= */
/**
* Convert exception-throwing code into Result-returning code.
*/
exports.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 = (0, exports.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 = (0, exports.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.
*/
exports.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) => exports.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) => exports.Try.async(() => cb(...args));
},
};