iron-enum
Version:
Rust like enums for Typescript
605 lines • 20.7 kB
TypeScript
/**
* # 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
*
* ---------------------------------------------------------------------------
*/
/**
* A record of possible variants, each key mapping to its associated data type.
*
* Example:
* ```ts
* type OptionVariants = { Some: string, None: undefined };
*
* // "Some" has an associated string, while "None" has no associated data (undefined).
* ```
*/
export type VariantsRecord = {
[K in Exclude<string, "_">]: any;
};
type EnumUnion<ALL extends VariantsRecord> = {
[K in keyof ALL & string]: ALL[K];
}[keyof ALL & string];
export type EnumFactoryUnion<ALL extends VariantsRecord> = {
[K in keyof ALL & string]: EnumFactory<K, ALL[K], ALL>;
}[keyof ALL & string];
/** `true` when `T` is an object *and* every key is optional */
type IsAllOptionalObject<T> = T extends object ? ({} extends T ? true : false) : false;
type VariantConstructor<Default, K extends string, ALL extends VariantsRecord> = [
Default
] extends [undefined | null | void] ? () => EnumFactory<K, Default, ALL> : IsAllOptionalObject<Default> extends true ? <P extends Default = Default>(data?: P) => EnumFactory<K, P, ALL> : <P extends Default>(data: P) => EnumFactory<K, P, ALL>;
/**
* Represents a single constructed enum value.
*
* - `tag` is the literal name of the variant.
* - `data` is the data associated with this variant (could be undefined).
* - It intersects with all of the enum methods (`EnumMethods`).
*
* Generics:
* - `TAG`: the specific variant key.
* - `PAYLOAD`: the associated data type for this key.
* - `ALL`: the entire `VariantsRecord` for reference by the methods.
*/
export type EnumFactory<TAG extends keyof ALL & string, PAYLOAD, ALL extends VariantsRecord> = {
tag: TAG;
data: EnumUnion<ALL>;
payload: PAYLOAD;
} & EnumMethods<ALL>;
/**
* A set of methods made available to every constructed enum value:
* - `toJSON`: Returns a plain object with exactly one key/value corresponding to the current variant.
* - `key`: Returns the variant key.
* - `if` / `ifNot`: Provide conditional checks against a specific variant.
* - `match` / `matchAsync`: Synchronously or asynchronously pattern-match on the variant key.
*/
export interface EnumMethods<ALL extends VariantsRecord> {
/**
* Returns a simple JavaScript object that has exactly one key, the variant name,
* and its associated data as the value.
*/
toJSON: () => Partial<ALL>;
/**
* Returns the variant key for this enum value.
*/
key: () => keyof ALL & string;
/**
* Provides an object with methods for handling a check against each variant:
*
* ```ts
* const value = MyEnum.Foo("some data");
*
* value.if.Foo(
* (payload) => console.log("This is Foo with data:", payload),
* (jsonObj) => console.log("This is not Foo; got:", jsonObj)
* );
* ```
*/
if: ObjectToIfMap<ALL>;
/**
* Provides an object with methods for handling the inverse check of a variant:
*
* ```ts
* const value = MyEnum.Foo("some data");
*
* value.ifNot.Bar(
* (jsonObj) => console.log("Value is not Bar; got:", jsonObj),
* (jsonObj) => console.log("Value is Bar; got:", jsonObj)
* );
* ```
*/
ifNot: ObjectToIfNotMap<ALL>;
/**
* Perform synchronous pattern matching on the variant. Must handle all variants or use `_` as a fallback.
*
* ```ts
* const result = MyEnum.Foo("some data").match({
* Foo: (payload) => `Got Foo with ${payload}`,
* Bar: (payload) => `Got Bar with ${payload}`,
* // fallback if not handled above
* _: () => "Unknown variant!"
* });
* ```
*/
match: <A extends MatchFns<ALL>>(callbacks: A) => MatchResult<A>;
/**
* Similar to `match` but allows asynchronous callback functions.
*
* ```ts
* const result = await MyEnum.Foo("some data").matchAsync({
* Foo: async (payload) => await processFoo(payload),
* Bar: async (payload) => await processBar(payload),
* _: async () => await handleUnknown()
* });
* ```
*/
matchAsync: <A extends MatchFnsAsync<ALL>>(callbacks: A) => Promise<MatchResult<A>>;
}
type NonOptional<T> = {
[K in keyof T]-?: T[K];
};
type ObjectToFunctionMapBase<T, R> = {
[K in keyof T]?: T[K] extends undefined | null ? () => R : (args: T[K]) => R;
};
type ObjectToFunctionMap<T> = ObjectToFunctionMapBase<T, any>;
type ObjectToFunctionMapAsync<T> = ObjectToFunctionMapBase<T, Promise<any>>;
/**
* Pattern matching map for each variant. Must specify all variant keys or have a "_" fallback.
*/
type MatchFns<X extends VariantsRecord> = NonOptional<ObjectToFunctionMap<X>> | (ObjectToFunctionMap<X> & {
_: () => any;
});
/**
* Pattern matching map for each variant with async callbacks. Must specify all variant keys or have a "_" fallback.
*/
type MatchFnsAsync<X extends VariantsRecord> = NonOptional<ObjectToFunctionMapAsync<X>> | (ObjectToFunctionMapAsync<X> & {
_: () => Promise<any>;
});
/**
* Derives the return type from the union of callback signatures in the map.
*/
type MatchResult<A> = A extends {
[K: string]: (...args: any) => infer R;
} ? R : never;
/**
* If the variant matches the key, call the `ifCallback`; otherwise call `elseCallback`.
* The return type is designed so if your callbacks return `void`,
* it simply returns a boolean indicating the success/failure of the check.
*/
type IfFnNull<T extends Record<string, any>> = <RIf = void, RElse = void>(ifCallback?: () => RIf, elseCallback?: (obj: Partial<T>) => RElse) => [RIf, RElse] extends [void, void] ? boolean : RIf extends void ? boolean | Exclude<RElse, void> : RElse extends void ? boolean | Exclude<RIf, void> : Exclude<RIf, void> | Exclude<RElse, void>;
/**
* Similar to `IfFnNull`, but for properties that are not null/undefined.
* The `ifCallback` receives the associated data, e.g. the `payload`.
*/
type IfFnArg<TValue, T extends Record<string, any>> = <RIf = void, RElse = void>(ifCallback?: (val: TValue) => RIf, elseCallback?: (jsonValue: Partial<T>) => RElse) => [RIf, RElse] extends [void, void] ? boolean : RIf extends void ? boolean | Exclude<RElse, void> : RElse extends void ? boolean | Exclude<RIf, void> : Exclude<RIf, void> | Exclude<RElse, void>;
/**
* The object type used by the `if` property on an enum instance.
* Each key corresponds to a potential variant key, returning a function
* that handles the if/else logic for that variant.
*/
type ObjectToIfMap<T extends Record<string, any>> = {
[K in keyof T]: T[K] extends null | undefined ? IfFnNull<T> : IfFnArg<T[K], T>;
};
/**
* `ifNot` logic handles the inverse check of a given key.
* The callback is invoked if the variant is NOT the specified key,
* else the `elseCallback` is invoked.
*/
type IfNotFn<TAll> = <RIf = void, RElse = void>(callback?: (jsonValue: Partial<TAll>) => RIf, elseCallback?: (jsonValue: Partial<TAll>) => RElse) => [RIf, RElse] extends [void, void] ? boolean : RIf extends void ? boolean | Exclude<RElse, void> : RElse extends void ? boolean | Exclude<RIf, void> : Exclude<RIf, void> | Exclude<RElse, void>;
/**
* The object type used by the `ifNot` property on an enum instance.
* Each key corresponds to a potential variant key, returning a function
* that handles the "not that variant" logic.
*/
type ObjectToIfNotMap<T> = {
[K in keyof T]: IfNotFn<T>;
};
/**
* Utility type that captures additional properties for a given Enum,
* including parse and type-checking utilities.
*/
export type EnumProperties<ALL extends VariantsRecord, AddedProps> = {
/**
* The variant keys available on this enum.
*/
typeKeys: keyof ALL;
/**
* The variants record used to construct this enum.
*/
typeVariants: Partial<ALL>;
/**
* The type of the Enum for usage in function arguments, etc.
*
* Example:
* ```ts
* const MyEnum = IronEnum<{ Foo: string, Bar: number }>();
* // The type of the entire enum instance:
* type MyEnumType = typeof MyEnum._.typeOf;
*
* function doSomething(value: MyEnumType) { ... }
* ```
*/
typeOf: EnumFactoryUnion<ALL> & AddedProps;
/**
* Reconstructs a variant from a plain object that must have exactly one key.
*
* Example:
* ```ts
* const dataObj = { Foo: "hello" };
* const myVariant = MyEnum._.parse(dataObj);
* // myVariant is an enum value with tag="Foo" and data="hello"
* ```
*/
parse(dataObj: Partial<ALL>): EnumFactory<keyof ALL & string, EnumUnion<ALL>, ALL>;
};
/**
* The shape of the object returned by `IronEnum`. It has one method per variant key
* that can construct that variant. It also has a special `_` property
* which is an object containing additional helpers (like `parse`).
*/
export type IronEnumInstance<ALL extends VariantsRecord> = {
[K in keyof ALL & string]: VariantConstructor<ALL[K], K, ALL>;
} & {
/**
* A special property containing meta-information and helper methods for the enum,
* such as `parse(...)`.
*/
_: EnumProperties<ALL, {}>;
};
/**
* 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.
*/
export declare function IronEnum<ALL extends VariantsRecord>(): "_" extends keyof ALL ? "ERROR: Cannot use '_' as a variant key!" : IronEnumInstance<ALL>;
/**
* Extends the basic enum instance with Rust-like methods for retrieving or transforming data.
* - `unwrap()`, `unwrap_or()`, `unwrap_or_else()`, `_try()`.
*/
type ExtendedRustMethods<T> = {
/**
* If this is a success/some variant, return the underlying data.
* Otherwise, throw an Error.
*/
unwrap: () => T;
/**
* If this is a success/some variant, return the data.
* Otherwise, return the provided default `value`.
*/
unwrap_or: <R>(value: R) => R | T;
/**
* If this is a success/some variant, return the data.
* Otherwise, return the result of `callback()`.
*/
unwrap_or_else: <R>(callback: () => R) => R | T;
};
/**
* Extends a Result type with an `ok()` method that transforms `Ok` data into a `Some` Option,
* or `Err` into a `None`.
*/
type ResultMethods<ALL extends {
Ok: unknown;
Err: unknown;
}> = {
/**
* Converts an `Ok` variant into `Some`, or an `Err` variant into `None`.
*/
ok: () => OptionFactory<{
Some: ALL["Ok"];
None: undefined;
}>;
/**
* Returns if the current Result is Ok.
*/
isOk: () => boolean;
/**
* Returns if the current Result is Err.
*/
isErr: () => boolean;
};
/**
* A specific type for a `Result`-style enum, with Rust-like methods for success/error handling.
*
* Example usage:
* ```ts
* const MyResult = Result<number, Error>();
* const success = MyResult.Ok(42);
* const failure = MyResult.Err(new Error("Oops"));
*
* success.unwrap() // 42
* failure.unwrap() // throws Error
* ```
*/
export type ResultFactory<ALL extends {
Ok: unknown;
Err: unknown;
}> = EnumFactory<keyof ALL & string, EnumUnion<ALL>, ALL> & ExtendedRustMethods<ALL["Ok"]> & ResultMethods<ALL>;
/**
* Returned by `Result<T, E>()`, it provides two methods for construction:
* - `Ok(...)`
* - `Err(...)`
* and a `._` property for parsing, plus all the extended Rust methods in the type definition.
*/
export type ResultInstance<ALL extends {
Ok: unknown;
Err: unknown;
}> = {
/**
* Constructs a successful variant with the provided data.
*/
Ok: (data: ALL["Ok"]) => ResultFactory<ALL>;
/**
* Constructs an error variant with the provided error data.
*/
Err: (data: ALL["Err"]) => ResultFactory<ALL>;
} & {
/**
* Provides the meta-information and parse function for the enum,
* as well as the extended Rust methods in the type signature.
*/
_: EnumProperties<ALL, ExtendedRustMethods<ALL["Ok"]> & ResultMethods<ALL>>;
};
/**
* 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
* ```
*/
export declare const Result: <T, E>() => ResultInstance<{
Ok: T;
Err: E;
}>;
/**
* 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
* ```
*/
export declare const Ok: <T>(value: T) => ResultFactory<{
Ok: T;
Err: unknown;
}>;
/**
* 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}`
* }));
* ```
*/
export declare const Err: <E>(error: E) => ResultFactory<{
Ok: unknown;
Err: E;
}>;
/**
* A specialized type for an Option-like enum, with Rust-like methods.
* Extends the base enum with `ok_or` / `ok_or_else` that transform an Option into a Result.
*/
export type OptionFactory<ALL extends {
Some: unknown;
None: undefined;
}> = EnumFactory<keyof ALL & string, EnumUnion<ALL>, ALL> & ExtendedRustMethods<ALL["Some"]> & OptionMethods<ALL["Some"]>;
type OptionMethods<OK> = {
/**
* If the variant is `Some`, return `Result.Ok(value)`. Otherwise `Result.Err(error)`.
*/
ok_or: <E>(error: E) => ResultFactory<{
Ok: OK;
Err: E;
}>;
/**
* Similar to `ok_or`, but takes a function that returns the error.
*/
ok_or_else: <E>(error: () => E) => ResultFactory<{
Ok: OK;
Err: E;
}>;
/**
* Returns is Option value is Some.
*/
isSome: () => boolean;
/**
* Returns is Option value is None
*/
isNone: () => boolean;
};
/**
* Returned by `Option<T>()`, it provides two methods for construction:
* - `Some(...)`
* - `None()`
* and a `._` property for parsing, plus all the extended Rust methods in the type definition.
*/
export type OptionInstance<ALL extends {
Some: unknown;
None: undefined;
}> = {
/**
* Construct a `Some` variant with associated data.
*/
Some: (data: ALL["Some"]) => OptionFactory<ALL>;
/**
* Construct a `None` variant with no associated data.
*/
None: () => OptionFactory<ALL>;
} & {
/**
* Provides the meta-information and parse function for the enum,
* as well as the extended Rust methods in the type signature.
*/
_: EnumProperties<ALL, ExtendedRustMethods<ALL["Some"]> & OptionMethods<ALL["Some"]>>;
};
/**
* 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
* ```
*/
export declare const Option: <T>() => OptionInstance<{
Some: T;
None: undefined;
}>;
/**
* A convenience function to construct `Option.Some` inline with a given value.
*
* ```ts
* const s = Some("Hello");
* console.log(s.unwrap()); // "Hello"
* ```
*/
export declare const Some: <T>(value: T) => OptionFactory<{
Some: T;
None: undefined;
}>;
/**
* A convenience function to construct `Option.None` inline with no associated data.
*
* ```ts
* const n = None();
* console.log(n.unwrap_or("default")); // "default"
* ```
*/
export declare const None: () => OptionFactory<{
Some: unknown;
None: undefined;
}>;
/**
* 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.
*/
export declare const Try: {
sync: <X>(callback: () => X) => ResultFactory<{
Ok: X;
Err: unknown;
}>;
async: <X>(callback: () => Promise<X>) => Promise<ResultFactory<{
Ok: X;
Err: unknown;
}>>;
};
/**
* 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.
*/
export declare const TryInto: {
sync: <X, Y extends any[]>(callback: (...args: Y) => X) => ((...args: Y) => ResultFactory<{
Ok: X;
Err: unknown;
}>);
async: <X, Y extends any[]>(callback: (...args: Y) => Promise<X>) => ((...args: Y) => Promise<ResultFactory<{
Ok: X;
Err: unknown;
}>>);
};
export {};
//# sourceMappingURL=mod.d.ts.map