UNPKG

iron-enum

Version:

Rust like enums for Typescript

807 lines 24.9 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 */ /** * Structure of enum variants. * * Each key is a variant name and its value is the payload for that variant. * Use `undefined` for variants without associated data. * * @example * type MyVariants = { * Unit: undefined; * Tuple: [string, number]; * Record: { id: string }; * } */ export type VariantsRecord = { readonly [K in Exclude<string, "_">]: unknown; }; /** * Union of all possible enum variant instances for a given `VariantsRecord`. * * This is useful for function parameters that accept any variant instance. * This type is typically accessed via the `_.typeOf` property on a factory. * * @example * const Status = IronEnum<{ Ok: string; Err: Error }>(); * * // The `_.typeOf` property provides this union type. * function logStatus(s: typeof Status._.typeOf) { * // s is: IronEnumVariantUnion<{ Ok: string; Err: Error }> * console.log(s.tag); * } */ export type IronEnumVariantUnion<ALL extends VariantsRecord> = { [K in keyof ALL & string]: IronEnumVariant<K, ALL[K], ALL>; }[keyof ALL & string]; /** * Constructor signature for a variant based on its payload type. * * - If the payload type is `undefined`, the constructor is nullary. * - Otherwise, the constructor requires the payload. */ type VariantConstructor<Default, K extends string, ALL extends VariantsRecord> = [ Default ] extends [undefined] ? () => IronEnumVariant<K, Default, ALL> : (data: Default) => IronEnumVariant<K, Default, ALL>; /** * Return type calculation used by `if` and `ifNot`. * * Produces a boolean when both callbacks return `void`. * Otherwise, returns the union of defined callback return types plus boolean. */ type IfReturn<RIf, 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>; /** * A single constructed enum variant instance with its discriminant, payload, * a back-reference to the factory that created it, and matching utilities. * * @template TAG The discriminant string literal * @template PAYLOAD The payload type carried by this variant * @template ALL The full `VariantsRecord` for the enum */ export type IronEnumVariant<TAG extends keyof ALL & string, PAYLOAD, ALL extends VariantsRecord> = { /** Discriminant of the variant. */ readonly tag: TAG; /** Payload carried by the variant. */ readonly data: PAYLOAD; /** * The factory object this instance originated from. * * @example * const loading = Status.Loading(); * // Create a new variant from the same factory * const ready = loading.instance.Ready({ finishedAt: new Date() }); */ readonly instance: IronEnumFactory<ALL>; } & EnumMethods<ALL, TAG>; /** * Serializable wire format for a variant instance. * * Use with `JSON.stringify` and `Enum._.parse` / `Enum._.fromJSON` / * `Enum._.reviver` to move values across the wire safely. * * @example * const s = Status.Error({ message: "oops", code: 500 }); * const json = JSON.stringify(s); * // json === '{"tag":"Error","data":{"message":"oops","code":500}}' */ export type IronEnumWireFormat<ALL extends VariantsRecord> = { [K in keyof ALL & string]: { tag: K; data: ALL[K]; }; }[keyof ALL & string]; /** * Helper to exclude a specific variant from the union by its tag. */ type ExcludeVariant<ALL extends VariantsRecord, K extends keyof ALL & string> = Exclude<IronEnumVariantUnion<ALL>, IronEnumVariant<K, ALL[K], ALL>>; /** * Methods present on each variant instance. * * Includes: * - `toJSON` for structured logging/serialization. * - `is` / `if` / `ifNot` for quick predicates. * - `match` / `matchAsync` for flexible pattern matching. * - `matchExhaustive` for compile-time exhaustive handling. */ export interface EnumMethods<ALL extends VariantsRecord, TAG extends keyof ALL & string> { /** * Convert the instance into `{ tag, data }` for JSON or debugging. * Automatically called by `JSON.stringify`. * * @example * const s = Status.Ready({ finishedAt: new Date() }); * const json = JSON.stringify(s); * // json === '{"tag":"Ready","data":{...}}' */ toJSON(): { readonly tag: TAG; readonly data: ALL[TAG]; }; /** * Predicate that narrows the variant type on success. * * @example * if (state.is("Ready")) { * // state.data is narrow to Ready payload * state.data.finishedAt.toISOString(); * } */ is<K extends keyof ALL & string>(key: K): this is IronEnumVariant<K, ALL[K], ALL>; /** * Execute a callback when the discriminant equals `key`. * * When no callbacks are provided, acts as a boolean predicate. * When callbacks are provided, returns `true` or the callback's * result on success, and `false` or the failure callback's * result on failure. * * The `failure` callback receives a type-safe union of all * variants *except* the one being checked. * * @example * // As a predicate * if (state.if("Loading")) { * // ... * } * * // With a success callback * state.if("Ready", (data) => { * console.log(data.finishedAt); * }); * * // With both callbacks to extract a value * const message = state.if( * "Error", * (data) => `Error: ${data.message}`, * (other) => `Status: ${other.tag}` // other is Loading | Ready * ); */ if<K extends keyof ALL & string, RIf = void, RElse = void>(key: K, success?: (payload: ALL[K], self: IronEnumVariant<K, ALL[K], ALL>) => RIf, failure?: (self: ExcludeVariant<ALL, K>) => RElse): IfReturn<RIf, RElse>; /** * Execute a callback when the discriminant does NOT equal `key`. * * The `success` callback receives a type-safe union of all * variants *except* the one being checked. * * @example * // As a predicate * if (state.ifNot("Error")) { * // state is Loading | Ready * } * * // With a success callback * state.ifNot("Loading", (other) => { * // other is Ready | Error * console.log(`Not loading: ${other.tag}`); * }); */ ifNot<K extends keyof ALL & string, RIf = void, RElse = void>(key: K, success?: (self: ExcludeVariant<ALL, K>) => RIf, failure?: (payload: ALL[K], self: IronEnumVariant<K, ALL[K], ALL>) => RElse): IfReturn<RIf, RElse>; /** * Pattern matching with optional `_` fallback arm. * * Use when some variants can be handled together via a fallback. * * @example * // Exhaustive match * const httpStatus = state.match({ * Loading: () => 202, * Ready: () => 200, * Error: ({ code }) => code, * }); * * // With a fallback * const message = state.match({ * Error: ({ message }) => message, * _: (self) => `Current state: ${self.tag}`, * }); */ match<A extends MatchFns<ALL>>(callbacks: A): MatchResult<A>; /** * Asynchronous pattern matching with optional `_` fallback arm. * All callbacks must return a `Promise`. * * @example * const data = await state.matchAsync({ * Loading: async () => fetch("/data"), * Ready: async (data) => data.finishedAt, * _: async () => null * }); */ matchAsync<A extends MatchFnsAsync<ALL>>(callbacks: A): Promise<MatchResult<A>>; /** * Exhaustive pattern matching. All variants must be handled. * * No `_` fallback is allowed. Compilation fails if a case is missing. * * @example * const message = state.matchExhaustive({ * Loading: () => "Still loading...", * Ready: ({ finishedAt }) => `Done at ${finishedAt}`, * Error: ({ message }) => `Failed: ${message}`, * }); */ matchExhaustive<A extends ExhaustiveFns<ALL>>(callbacks: A): MatchResult<A>; } type NonOptional<T> = { [K in keyof T]-?: T[K]; }; /** Map each tag to a synchronous handler `(payload, self) => R`. */ type ObjectToFunctionMapBase<T extends VariantsRecord, R> = { [K in keyof T]?: (payload: T[K], self: IronEnumVariant<K & string, T[K], T>) => R; }; /** Sync handler options. */ type ObjectToFunctionMap<T extends VariantsRecord> = ObjectToFunctionMapBase<T, any>; /** Async handler options. */ type ObjectToFunctionMapAsync<T extends VariantsRecord> = ObjectToFunctionMapBase<T, Promise<any>>; /** * Valid sync match configurations: * 1) Provide all tags, or * 2) Provide partial tags plus `_` fallback. */ type MatchFns<X extends VariantsRecord> = NonOptional<ObjectToFunctionMap<X>> | (ObjectToFunctionMap<X> & { _: (self: IronEnumVariantUnion<X>) => any; }); /** * Valid async match configurations: * 1) Provide all tags, or * 2) Provide partial tags plus `_` fallback. */ type MatchFnsAsync<X extends VariantsRecord> = NonOptional<ObjectToFunctionMapAsync<X>> | (ObjectToFunctionMapAsync<X> & { _: (self: IronEnumVariantUnion<X>) => Promise<any>; }); /** Exhaustive mapping: all tags required, no `_` fallback allowed. */ type ExhaustiveFns<X extends VariantsRecord> = { [K in keyof X & string]: (payload: X[K], self: IronEnumVariant<K, X[K], X>) => any; }; /** Extract the unified return type of a match dispatch. */ type MatchResult<A> = A extends { [K: string]: (...args: any) => infer R; } ? R : never; /** * Runtime factory for an enum, where each key is a constructor for its variant. * * @example * const Status = IronEnum<{ * Loading: undefined; * Ready: { count: number }; * }>(); * * const s1 = Status.Loading(); // { tag: "Loading", data: undefined } * const s2 = Status.Ready({ count: 1 }); // { tag: "Ready", data: { count: 1 } } */ export type IronEnumFactory<ALL extends VariantsRecord> = { [K in keyof ALL & string]: VariantConstructor<ALL[K], K, ALL>; } & { /** * Utilities and [TYPE ONLY] metadata exposed via the `_` property. * * The fields marked [TYPE ONLY] exist only for type access in code and do not * exist at runtime. They intentionally evaluate to `never` at runtime. */ _: EnumProperties<ALL, {}>; }; /** * Utility metadata exposed on the factory via `_`. * * Includes parsers and [TYPE ONLY] accessors for tags, payload union, and * the union instance type. */ export type EnumProperties<ALL extends VariantsRecord, AddedProps> = { /** * [TYPE ONLY] Union of tag names. * * @example * const Status = IronEnum<{ A: 0; B: 1 }>(); * type StatusTags = typeof Status._.typeTags; // "A" | "B" */ readonly typeTags: keyof ALL & string; /** * [TYPE ONLY] Union of payload values. * * @example * const Status = IronEnum<{ A: 0; B: 1 }>(); * type StatusData = typeof Status._.typeData; // 0 | 1 */ readonly typeData: ALL[keyof ALL]; /** * [TYPE ONLY] The generic json format exported by `toJSON` and parsed by `_.parse`. * * @example * const Status = IronEnum<{ A: 0; B: 1 }>(); * * function process(s: typeof Status._.typeJson) { * const p = Status._.parse(s); * } * * process(Status.A(0).toJSON()); * */ readonly typeJson: IronEnumWireFormat<ALL>; /** * [TYPE ONLY] Union of all variant instances for this enum. * This is the main type to use in function signatures or return statements. * * @example * const Status = IronEnum<{ A: 0; B: 1 }>(); * * function process(s: typeof Status._.typeOf) { * // ... * } * * process(Status.A(0)); */ readonly typeOf: IronEnumVariantUnion<ALL> & AddedProps; /** * Parse `{ tag, data }` into a variant instance. * If the keys where provided to the original function call then throws when the tag is not recognized by this factory. * * @example * const data = { tag: "Ready", data: { finishedAt: new Date() } } as const; * const s = Status._.parse(data); * if (s.is("Ready")) { ... } */ parse<TAG extends keyof ALL & string>(dataObj: ALL[TAG] extends undefined ? { tag: TAG; data?: ALL[TAG]; } : { tag: TAG; data: ALL[TAG]; }): IronEnumVariant<TAG, ALL[TAG], ALL>; /** * Alias of `parse`. Convenient for deserializers. * @see parse */ fromJSON<TAG extends keyof ALL & string>(dataObj: ALL[TAG] extends undefined ? { tag: TAG; data?: ALL[TAG]; } : { tag: TAG; data: ALL[TAG]; }): IronEnumVariant<TAG, ALL[TAG], ALL>; /** * JSON.parse reviver. Pass as the reviver to automatically convert * nested `{ tag, data }` shapes for this enum. * * @example * const text = '{"tag":"Ready","data":{...}}'; * const s = JSON.parse(text, (k, v) => Status._.reviver(v)); * // s is now a full Status.Ready variant instance */ reviver(obj: unknown): IronEnumVariantUnion<ALL> | unknown; }; /** * 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 declare function IronEnum<ALL extends VariantsRecord>(args?: { keys?: (keyof ALL & string)[]; }): "_" extends keyof ALL ? "ERROR: '_' is reserved!" : IronEnumFactory<ALL>; /** * Methods common to success-carrying types that allow extracting values, * defaults, or lazily computed fallbacks. */ type ExtendedRustMethods<T> = { /** * Return the success value. * **Throws** if the variant is `Err` or `None`. * * @example * Ok(1).unwrap(); // 1 * Err("!").unwrap(); // Throws * None().unwrap(); // Throws */ unwrap(): T; /** * Return the success value or a provided default. * * @example * Ok(1).unwrap_or(0); // 1 * Err("!").unwrap_or(0); // 0 * None().unwrap_or(0); // 0 */ unwrap_or<R>(value: R): R | T; /** * Return the success value or the result of a fallback function. * The callback is only executed if needed. * * @example * Ok(1).unwrap_or_else(() => 0); // 1 * Err("!").unwrap_or_else(() => 0); // 0 * None().unwrap_or_else(() => expensive_calc()); // 0 */ unwrap_or_else<R>(cb: () => R): R | T; }; /** * Result-specific helpers for mapping and chaining. */ type ResultMethods<ALL extends { Ok: unknown; Err: unknown; }> = { /** * Convert `Result<T,E>` to `Option<T>`, dropping the error. * * @example * Ok(1).ok(); // Some(1) * Err("!").ok(); // None() */ ok(): OptionVariant<{ Some: ALL["Ok"]; None: undefined; }>; /** * Predicate for `Ok` variant. * * @example * if (myResult.isOk()) { ... } */ isOk(): boolean; /** * Predicate for `Err` variant. * * @example * if (myResult.isErr()) { ... } */ isErr(): boolean; /** * Map the `Ok` value, leaving `Err` untouched. * * @example * Ok(1).map(x => x + 1); // Ok(2) * Err("!").map(x => x + 1); // Err("!") */ map<U>(f: (t: ALL["Ok"]) => U): ResultVariant<{ Ok: U; Err: ALL["Err"]; }>; /** * Map the `Err` value, leaving `Ok` untouched. * * @example * Ok(1).mapErr(x => x + "!"); // Ok(1) * Err("!").mapErr(x => x + "!"); // Err("!!") */ mapErr<F>(f: (e: ALL["Err"]) => F): ResultVariant<{ Ok: ALL["Ok"]; Err: F; }>; /** * Chain a new `Result`-returning operation when `Ok`. * Also known as `flatMap`. * * @example * const safeDivide = (n: number) => n === 0 ? Err("div by 0") : Ok(10 / n); * * Ok(5).andThen(safeDivide); // Ok(2) * Ok(0).andThen(safeDivide); // Err("div by 0") * Err("!").andThen(safeDivide); // Err("!") */ andThen<U>(f: (t: ALL["Ok"]) => ResultVariant<{ Ok: U; Err: ALL["Err"]; }>): ResultVariant<{ Ok: U; Err: ALL["Err"]; }>; }; /** * A Result instance (Ok or Err) with extraction and mapping helpers. * * This type is invariant, meaning `Result<T, never>` is not assignable * to `Result<T, string>`. * * To accept `Result` in a function, either use a specific factory: * @example * const MyResult = Result<string, Error>(); * function process(r: typeof MyResult._.typeOf) { ... } * * // Or make the function generic: * function process<T, E>(r: ResultVariant<{ Ok: T, Err: E }>) { ... } */ export type ResultVariant<ALL extends { Ok: unknown; Err: unknown; }> = IronEnumVariant<keyof ALL & string, ALL[keyof ALL], ALL> & ExtendedRustMethods<ALL["Ok"]> & ResultMethods<ALL>; /** * Factory for creating Ok and Err variants plus metadata via `_`. * * @example * const R = Result<number, string>(); * const r1 = R.Ok(1); * const r2 = R.Err("nope"); */ export type ResultFactory<ALL extends { Ok: unknown; Err: unknown; }> = { Ok(data: ALL["Ok"]): IronEnumVariant<"Ok", ALL["Ok"], ALL> & ExtendedRustMethods<ALL["Ok"]> & ResultMethods<ALL>; Err(data: ALL["Err"]): IronEnumVariant<"Err", ALL["Err"], ALL> & ExtendedRustMethods<ALL["Ok"]> & ResultMethods<ALL>; _: EnumProperties<ALL, ExtendedRustMethods<ALL["Ok"]> & ResultMethods<ALL>>; }; /** * 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 declare const Result: <T, E>() => ResultFactory<{ Ok: T; Err: E; }>; /** * Convenience Ok constructor for ad-hoc success values. * The `Err` type is `never`. * * @example * const r = Ok(123); // ResultVariant<{ Ok: number, Err: never }> */ export declare const Ok: <T>(value: T) => ResultVariant<{ Ok: T; Err: never; }>; /** * Convenience Err constructor for ad-hoc error values. * The `Ok` type is `never`. * * @example * const r = Err("oops"); // ResultVariant<{ Ok: never, Err: string }> */ export declare const Err: <E>(error: E) => ResultVariant<{ Ok: never; Err: E; }>; /** * Option-specific helpers for converting to Result, mapping, chaining, and * basic predicates. */ type OptionMethods<OK> = { /** * Convert `Some(T)` to `Ok(T)` or `None` to `Err(E)`. * * @example * Some(1).ok_or("!"); // Ok(1) * None().ok_or("!"); // Err("!") */ ok_or<E>(err: E): ResultVariant<{ Ok: OK; Err: E; }>; /** * Convert `Some(T)` to `Ok(T)` or `None` to `Err(E)` * using a lazily-computed error. * * @example * Some(1).ok_or_else(() => "!"); // Ok(1) * None().ok_or_else(() => "!"); // Err("!") */ ok_or_else<E>(err: () => E): ResultVariant<{ Ok: OK; Err: E; }>; /** * Predicate for `Some` variant. * * @example * if (myOption.isSome()) { ... } */ isSome(): boolean; /** * Predicate for `None` variant. * * @example * if (myOption.isNone()) { ... } */ isNone(): boolean; /** * Map the `Some` value, leaving `None` untouched. * * @example * Some(1).map(x => x + 1); // Some(2) * None().map(x => x + 1); // None() */ map<U>(f: (t: OK) => U): OptionVariant<{ Some: U; None: undefined; }>; /** * Chain a new `Option`-returning operation when `Some`. * Also known as `flatMap`. * * @example * const firstChar = (s: string) => s.length > 0 ? Some(s[0]) : None(); * * Some("hi").andThen(firstChar); // Some("h") * Some("").andThen(firstChar); // None() * None().andThen(firstChar); // None() */ andThen<U>(f: (t: OK) => OptionVariant<{ Some: U; None: undefined; }>): OptionVariant<{ Some: U; None: undefined; }>; /** * Keep `Some` only when the predicate passes. * * @example * Some(2).filter(x => x % 2 === 0); // Some(2) * Some(1).filter(x => x % 2 === 0); // None() * None().filter(x => x % 2 === 0); // None() */ filter(p: (t: OK) => boolean): OptionVariant<{ Some: OK; None: undefined; }>; }; /** * Option instance (Some or None) enriched with extraction and mapping helpers. * * This type is invariant. See `ResultVariant` for notes on usage. */ export type OptionVariant<ALL extends { Some: unknown; None: undefined; }> = IronEnumVariant<keyof ALL & string, ALL[keyof ALL], ALL> & ExtendedRustMethods<ALL["Some"]> & OptionMethods<ALL["Some"]>; /** * Factory for creating Some and None variants plus metadata via `_`. * * @example * const O = Option<number>(); * const s = O.Some(1); * const n = O.None(); */ export type OptionFactory<ALL extends { Some: unknown; None: undefined; }> = { Some(data: ALL["Some"]): IronEnumVariant<"Some", ALL["Some"], ALL> & ExtendedRustMethods<ALL["Some"]> & OptionMethods<ALL["Some"]>; None(): IronEnumVariant<"None", ALL["None"], ALL> & ExtendedRustMethods<ALL["Some"]> & OptionMethods<ALL["Some"]>; _: EnumProperties<ALL, ExtendedRustMethods<ALL["Some"]> & OptionMethods<ALL["Some"]>>; }; /** * Create a typed Option factory `<T>`. * * @example * const NumberOption = Option<number>(); * const s = NumberOption.Some(100); * * function process(o: typeof NumberOption._.typeOf) { * // ... * } */ export declare const Option: <T>() => OptionFactory<{ Some: T; None: undefined; }>; /** * Convenience Some constructor for ad-hoc values. * * @example * const s = Some(123); // OptionVariant<{ Some: number, ... }> */ export declare const Some: <T>(value: T) => OptionVariant<{ Some: T; None: undefined; }>; /** * Convenience None constructor. * The `Some` type is `never`. * * @example * const n = None(); // OptionVariant<{ Some: never, ... }> */ export declare const None: () => OptionVariant<{ Some: never; None: undefined; }>; /** * Convert exception-throwing code into Result-returning code. */ export declare const Try: { /** * Execute a synchronous function and wrap the outcome. * Ok on success, Err with the caught exception on failure. */ sync<X>(cb: () => X): ResultVariant<{ Ok: X; Err: unknown; }>; /** * Execute an asynchronous function and wrap the outcome. * Resolves to Ok on success, Err with the caught exception on rejection. */ async<X>(cb: () => Promise<X>): Promise<ResultVariant<{ Ok: X; Err: unknown; }>>; }; /** * Transform functions to return Result instead of throwing. */ export declare const TryInto: { /** * Wrap a synchronous function. The wrapper never throws. */ sync<X, Y extends any[]>(cb: (...args: Y) => X): (...args: Y) => ResultVariant<{ Ok: X; Err: unknown; }>; /** * Wrap an asynchronous function. The wrapper resolves to Result. */ async<X, Y extends any[]>(cb: (...args: Y) => Promise<X>): (...args: Y) => Promise<ResultVariant<{ Ok: X; Err: unknown; }>>; }; export {}; //# sourceMappingURL=mod.d.ts.map