ts-enum-util

repository·master·Indexed 42 days ago

https://github.com/uselesspickles/ts-enum-util

TypeScript Enum Utilities providing strictly typed tools for enum inspection and exhaustive value handling. Features include an EnumWrapper for retrieving keys, values, and reverse lookups, as well as a visitor pattern via $enum.visitValue and $enum.mapValue to ensure all enum cases are handled at compile time. Supports TypeScript 2.9+ and both string and numeric enums.

Tokens
13.2K
Snippets
47
Records
61
Agent score
54%

What's inside ts-enum-util

  1. What is ts-enum-util?

    master

    ts-enum-util provides type-safe utilities for working with TypeScript enums and string/number literal union types. It is divided into two main functional areas:

    1. Enum Wrapper Utilities: Provides a wrapper around an enum (an "enum-like object") to perform run-time operations like retrieving keys/values, reverse lookups, and validation with full type inference.
    2. Enum Value Visitor/Mapper: Implements a visitor pattern for processing single enum values or union types. It acts like a type-safe switch statement that forces you to handle every possible case, preventing bugs when enums are updated.
  2. What is an EnumWrapper and how do I use it?

    master

    An EnumWrapper is a class that wraps an "enum-like" object (any object where property values are string or number) to provide utility methods.

    While you can reference the class directly, you should almost always use the $enum() function to obtain an instance. The EnumWrapper is generically typed, meaning methods like getKeys() return specific string literal unions of the actual keys in your enum, providing high type safety.

    Key Characteristics:

    • Map-like: Implements size, keys(), values(), entries(), forEach(), and is iterable. Note that has() and get() are replaced by isKey() and getValueOrDefault() for naming consistency.
    • Array-like: Implements the ArrayLike interface, allowing it to be used with libraries like lodash that expect array-like collections of EnumWrapper.Entry objects.
    • Order Preserving: It attempts to retain the original definition order of the enum entries.
    // Use $enum() to get an EnumWrapper instance
    const wrapper = $enum(MyEnum);
  3. Handle null and undefined in visitors and mappers

    master

    If your input type includes null or undefined, you must explicitly handle these cases in your visitor or mapper implementation using unique symbols provided by $enum to avoid naming collisions.

    • Use [$enum.handleNull] to handle null values.
    • Use [$enum.handleUndefined] to handle undefined values.

    If the input type does not include these, you do not need to provide these properties.

    import { $enum } from "ts-enum-util";
    
    type RGB = "r" | "g" | "b";
    
    // Handling null in a Visitor
    function getRgbLabel(rgb: RGB | null): string {
        return $enum.visitValue(rgb).with({
            r: () => "Red",
            g: () => "Green",
            b: () => "Blue",
            [$enum.handleNull]: () => "null"
        });
    }
    
    // Handling null in a Mapper
    function getRgbLabelMap(rgb: RGB | null): string {
        return $enum.mapValue(rgb).with({
            r: "Red",
            g: "Green",
            b: "Blue",
            [$enum.handleNull]: "null"
        });
    }
  4. How caching works with $enum()

    master

    EnumWrapper instances are cached using an ES6 WeakMap. When you call $enum(MyEnum), the library checks if a wrapper for that specific object already exists.

    Benefits:

    • Convenience: You can call $enum() anywhere in your codebase without manually managing or passing around a single EnumWrapper instance.
    • Memory Safety: Because it uses WeakMap, the cached wrapper will be garbage collected automatically when the underlying enum object is garbage collected, preventing memory leaks.

    Performance Tip: While WeakMap lookups are typically O(1) (constant time), they are not free. If you are using EnumWrapper methods inside a tight loop, store the result of $enum() in a local variable instead of calling $enum() repeatedly.

  5. Understand Visitor method parameter types

    master

    In a Visitor implementation, the callback function for each property receives a single parameter: the value being visited. The type of this parameter is exactly the type of the specific value handled by that method, not the entire union type.

    For example, if visiting a union type "r" | "g" | "b", the handler for r receives a parameter of type exactly "r".

    type RGB = "r" | "g" | "b";
    
    function rgbIdentity(rgb: RGB | null | undefined): RGB | null | undefined {
        return $enum.visitValue(rgb).with({
            r: (value) => {
                // type of 'value' is exactly "r"
                return value;
            },
            [$enum.handleNull]: (value) => {
                // type of 'value' is exactly null
                return value;
            }
        });
    }
  6. Visit and Map Enum values (not names)

    master

    When using $enum.visitValue() or $enum.mapValue(), the keys in your configuration object must correspond to the values of the enum members, not their identifiers (names).

    For an enum enum Color { Red = "r" }, you must use "r" or Color.Red as the key, not "Red".

    enum RGB {
        R = "r",
        G = "g",
        B = "b"
    }
    
    function getRgbLabel(rgb: RGB): string {
        return $enum.visitValue(rgb).with<string>({
            [RGB.R]: () => "Red", // Correct: uses the value "r"
            g: () => "Green",    // Correct: uses the literal value "g"
            // B: () => "Blue"   // Incorrect: B is the name, not the value
        });
    }
  7. Iterate over EnumWrapper entries

    master

    An EnumWrapper implements several iteration interfaces, allowing you to treat it like a Map or an Array.

    Map-like Iteration

    You can use standard Map-like methods to iterate over keys, values, or both:

    • keys(): Returns an IterableIterator<KeyType>.
    • values(): Returns an IterableIterator<EnumType>.
    • entries(): Returns an IterableIterator<EnumWrapper.Entry>.
    • forEach(iteratee, context): Executes a callback for every entry.
    • @@iterator: Allows direct use in for...of loops (iterates over [key, value] tuples).

    Array-like Access

    EnumWrapper supports indexed access. You can access [key, value] tuples by their numeric index (e.g., wrapper[0]). The length property returns the total number of entries.

    Iteratee Signature

    When using forEach or map, the callback (iteratee) receives: (value, key, enumWrapper, index) => R

    // Example of for...of iteration
    for (const [key, value] of wrapper.entries()) {
      console.log(key, value);
    }
    
    // Example of map
    const keysArray = wrapper.map((val, key) => key);
  8. Use EnumWrapper as an Array-Like object

    master

    An EnumWrapper behaves like a read-only array of [key, value] tuples. You can access elements by index and check the length property.

    • length: The number of entries in the enum.
    • [index]: Accesses the entry at the specified index.
    const wrappedRgb = $enum(RGB);
    
    // type: number
    const length = wrappedRgb.length;
    
    // type: [("R" | "G" | "B"), RGB]
    const entry = wrappedRgb[1];
  9. Use EnumWrapper as a Map-Like object

    master

    An EnumWrapper behaves like a read-only Map where keys are the enum names and values are the enum values. It implements the standard Map interface for iteration.

    • size: The number of entries.
    • keys(): Returns an iterator for the keys.
    • values(): Returns an iterator for the values.
    • entries(): Returns an iterator for [key, value] pairs.
    • forEach(callback): Iterates over entries. Note that the callback signature (value, key, wrappedEnum, index) includes an extra index parameter compared to standard Map.forEach.

    Note: The EnumWrapper does not implement .get() or .has() methods directly; use the lookup methods (e.g., getValueOrThrow) instead.

    const wrappedRgb = $enum(RGB);
    
    // type: number
    const size = wrappedRgb.size;
    
    // Directly iterable like a Map
    for (const [key, value] of wrappedRgb) {
        // type of key: ("R" | "G" | "B")
        // type of value: RGB
    }
    
    for (const key of wrappedRgb.keys()) {
        // type of key: ("R" | "G" | "B")
    }
    
    for (const value of wrappedRgb.values()) {
        // type of value: RGB
    }
  10. How iteration order works in EnumWrapper

    master

    The EnumWrapper attempts to preserve the order in which enum entries were defined in the source code.

    In environments with an ES6-compliant Object.getOwnPropertyNames implementation, the order of iteration is guaranteed to match the source code definition. In older environments, while not strictly guaranteed by the spec, the library provides high practical confidence that the definition order is retained by processing only own, enumerable, non-array-index properties.

    enum ABC = {
        B = 3,
        A = 1,
        C = 2
    }
    
    // defined order is retained: ["B", "A", "C"]
    const keys = $enum(ABC).getKeys();
    
    // defined order is retained: [3, 1, 2]
    const values = $enum(ABC).getValues();
  11. Handle unexpected runtime values

    master

    When processing data from external sources (like APIs), values might not match your TypeScript types at runtime. By default, $enum.visitValue and $enum.mapValue will throw an error if an unexpected value is encountered.

    To override this behavior and provide a custom fallback, use the [$enum.handleUnexpected] property.

    • In a Visitor, [$enum.handleUnexpected] is a function.
    • In a Mapper, [$enum.handleUnexpected] is a value.
    import { $enum } from "ts-enum-util";
    
    type RGB = "r" | "g" | "b";
    
    // Visitor with custom unexpected handler
    function getRgbLabel(rgb: RGB): string {
        return $enum.visitValue(rgb).with({
            r: () => "Red",
            g: () => "Green",
            b: () => "Blue",
            [$enum.handleUnexpected]: () => "Unexpected!"
        });
    }
    
    // Mapper with custom unexpected handler
    function getRgbLabelMap(rgb: RGB): string {
        return $enum.mapValue(rgb).with({
            r: "Red",
            g: "Green",
            b: "Blue",
            [$enum.handleUnexpected]: "Unexpected!"
        });
    }
  12. Share visitor methods across multiple enum values

    master

    To avoid code duplication, you can define shared handler functions. How you type these functions depends on whether they need to access the value being visited:

    1. Without value access: If the handler doesn't use the parameter, it can be a simple function with no arguments.
    2. With value access: If the handler uses the parameter, the parameter type must be broad enough to encompass all enum values (and potentially null or undefined) that the handler will be applied to.
    type RGB = "r" | "g" | "b";
    
    function isSupportedColor(rgb: RGB | null | undefined): boolean {
        // Shared handler that uses the value
        // Must include the union type to be safe across multiple keys
        const handleSupported = (value: RGB): boolean => {
            console.log(`handling: ${value.toUpperCase()}`);
            return true;
        };
    
        // Shared handler for null/undefined
        const handleUnsupported = (value: RGB | null | undefined): boolean => {
            console.warn(`unsupported: ${value}`);
            return false;
        };
    
        return $enum.visitValue(rgb).with<boolean>({
            r: handleSupported,
            g: handleUnsupported,
            b: handleSupported,
            [$enum.handleNull]: handleUnsupported,
            [$enum.handleUndefined]: handleUnsupported
        });
    }