Skip to content

Instantly share code, notes, and snippets.

@trvswgnr
Last active February 9, 2025 06:06
Show Gist options
  • Save trvswgnr/f8d4b65176cd03e979389cf5c9ff57d9 to your computer and use it in GitHub Desktop.
Save trvswgnr/f8d4b65176cd03e979389cf5c9ff57d9 to your computer and use it in GitHub Desktop.

Revisions

  1. trvswgnr revised this gist Feb 8, 2025. 1 changed file with 79 additions and 78 deletions.
    157 changes: 79 additions & 78 deletions enum.ts
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,63 @@
    // --- examples --- //

    // happy path
    const Priority = Enum("Low", "Normal", "High");
    // ^?
    type Priority = Enum<typeof Priority>;
    // ^?

    // invalid key starting with number
    const InvalidPriority1 = Enum("1Low", "Normal", "High");
    // ^?
    type InvalidPriority1 = Enum<typeof InvalidPriority1>;
    // ^?

    // invalid key with special character
    const InvalidPriority2 = Enum("Low", "Normal", "H!gh");
    // ^?
    type InvalidPriority2 = Enum<typeof InvalidPriority2>;
    // ^?

    // invalid duplicate key
    const InvalidPriority3 = Enum("Low", "Normal", "Low");
    // ^?
    type InvalidPriority3 = Enum<typeof InvalidPriority3>;
    // ^?

    const Status = Enum("Status", { Open: "o", Closed: "c" });
    // ^?
    type Status = Enum<typeof Status>;
    // ^?


    // --- implementation --- //

    /** creates a numeric enum */
    function Enum<const K0 extends string, const T extends readonly string[]>(
    k0: ValidateEnumKey<K0>,
    ...keys: UniqueAndValid<K0, T>
    ): Prettify<NumericEnumReturn<K0, T>>;
    /** creates a string enum */
    function Enum<const T extends Record<string, string>, const B extends string>(
    name: B,
    t: T,
    ): Prettify<StringEnumReturn<T, B>>;
    // biome-ignore lint/suspicious/noExplicitAny: i like to party
    function Enum(...args: any[]) {
    if (typeof args[0] === "string" && typeof args[1] === "object") {
    return args[1];
    }
    return NumericEnum(args[0], ...(args.slice(1) as never));
    }
    // biome-ignore lint/suspicious/noRedeclare: intentionally redeclaring Enum
    type Enum<T> = [T] extends [never]
    ? TSError<"Invalid Enum">
    : T extends Record<string, string>
    ? StringEnum<T>
    : T extends Record<string, number>
    ? NumericEnum<T>
    : never;

    declare const TS_ERROR: unique symbol;
    /** used for showing errors in typescript */
    type TSError<Message extends string, Satistifes = unknown> = Satistifes & {
    @@ -12,6 +72,16 @@ type StringToTuple<S extends string> = S extends `${infer F}${infer R}`
    ? [F, ...StringToTuple<R>]
    : [];

    /** checks if a value is in a tuple */
    type Includes<T extends readonly unknown[], U> = T extends [
    infer First,
    ...infer Rest,
    ]
    ? First extends U
    ? true
    : Includes<Rest, U>
    : false;

    /** checks if string includes character */
    type StringIncludes<Str extends string, Char extends string> = Includes<
    StringToTuple<Str>,
    @@ -31,12 +101,9 @@ type Alpha = AlphaLower | AlphaUpper;
    /** valid starting characters for an enum key */
    type ValidStart = Alpha | "_" | "$";

    /** checks if string starts with an alphabetic character */
    type StartsWithAlpha<T extends string> = StartsWith<T, ValidStart>;

    // biome-ignore format: long union type would be many lines
    /** a non-exhaustive list of special characters that are invalid in enum keys */
    type InvalidSpecialChars = "!" | "@" | "#" | "%" | "^" | "&" | "*" | "(" | ")" | "-" | "=" | "+" | "[" | "]" | "{" | "}" | ";" | ":" | "'" | '"' | "," | "." | "<" | ">" | "/" | "?" | "\\" | "|" | "`" | "~";
    type InvalidSpecialChars = " " | "!" | "@" | "#" | "%" | "^" | "&" | "*" | "(" | ")" | "-" | "=" | "+" | "[" | "]" | "{" | "}" | ";" | ":" | "'" | '"' | "," | "." | "<" | ">" | "/" | "?" | "\\" | "|" | "`" | "~";

    /** gets the index of a value in a tuple */
    type IndexOf<T extends readonly unknown[], V> = T extends [
    @@ -48,16 +115,6 @@ type IndexOf<T extends readonly unknown[], V> = T extends [
    : IndexOf<Rest, V>
    : never;

    /** checks if a value is in a tuple */
    type Includes<T extends readonly unknown[], U> = T extends [
    infer First,
    ...infer Rest,
    ]
    ? First extends U
    ? true
    : Includes<Rest, U>
    : false;

    /** validates uniqueness of a tuple */
    type IsUnique<
    T extends readonly unknown[],
    @@ -69,11 +126,11 @@ type IsUnique<
    : true;

    /** validates a key in an enum, returning a custom ts error if invalid */
    type ValidateEnumKey<T extends string> = StartsWithAlpha<T> extends true
    type ValidateEnumKey<T extends string> = StartsWith<T, ValidStart> extends true
    ? StringIncludes<T, InvalidSpecialChars> extends false
    ? T
    : TSError<"Keys must not contain special characters">
    : TSError<"Keys must start with an alphabetic character">;
    : TSError<"Keys must not contain invalid special characters">
    : TSError<"Keys must start with a letter, underscore (_), or dollar sign ($)">;

    /** checks if all keys in a tuple are valid enum keys */
    type AllKeysValid<T extends readonly string[]> = T extends [
    @@ -91,7 +148,7 @@ type UniqueAndValid<K0 extends string, T extends readonly string[]> = IsUnique<[
    ? AllKeysValid<[K0, ...T]> extends true
    ? T
    : TSError<
    "One or more keys are invalid. Keys must start with an alphabetic character and not contain special characters",
    "One or more keys are invalid. Keys must start with a letter, underscore (_), or dollar sign ($), and not contain other special characters",
    readonly string[]
    >
    : TSError<"Keys must be unique", readonly string[]>;
    @@ -118,7 +175,10 @@ function NumericEnum<
    }
    return x;
    }
    type NumericEnum<T extends Record<string, unknown>> = Extract<T[keyof T], number>;
    type NumericEnum<T extends Record<string, unknown>> = Extract<
    T[keyof T],
    number
    >;

    declare const VARIANT: unique symbol;
    /** a branded type for variants of a string enum */
    @@ -137,62 +197,3 @@ function StringEnum<
    }
    /** union of all string enum values */
    type StringEnum<T extends Record<string, string>> = Extract<T[keyof T], string>;

    /** creates a numeric enum */
    function Enum<const K0 extends string, const T extends readonly string[]>(
    k0: ValidateEnumKey<K0>,
    ...keys: UniqueAndValid<K0, T>
    ): Prettify<NumericEnumReturn<K0, T>>;
    /** creates a string enum */
    function Enum<const T extends Record<string, string>, const B extends string>(
    name: B,
    t: T,
    ): Prettify<StringEnumReturn<T, B>>;
    // biome-ignore lint/suspicious/noExplicitAny: <explanation>
    function Enum(...args: any[]) {
    if (typeof args[0] === "string" && typeof args[1] === "object") {
    return args[1];
    }
    return NumericEnum(args[0], ...(args.slice(1) as never));
    }
    // biome-ignore lint/suspicious/noRedeclare: intentionally redeclaring Enum
    type Enum<T> = [T] extends [never]
    ? TSError<"Invalid Enum">
    : T extends Record<string, string>
    ? StringEnum<T>
    : T extends Record<string, number>
    ? NumericEnum<T>
    : never;


    // examples

    // happy path
    const Priority = Enum("Low", "Normal", "High");
    // ^?
    type Priority = Enum<typeof Priority>;
    // ^?

    // invalid key starting with number
    const InvalidPriority1 = Enum("1Low", "Normal", "High");
    // ^?
    type InvalidPriority1 = Enum<typeof InvalidPriority1>;
    // ^?

    // invalid key with special character
    const InvalidPriority2 = Enum("Low", "Normal", "H!gh");
    // ^?
    type InvalidPriority2 = Enum<typeof InvalidPriority2>;
    // ^?

    // invalid duplicate key
    const InvalidPriority3 = Enum("Low", "Normal", "Low");
    // ^?
    type InvalidPriority3 = Enum<typeof InvalidPriority3>;
    // ^?


    const Status = Enum("Status", { Open: "o", Closed: "c" });
    // ^?
    type Status = Enum<typeof Status>;
    // ^?
  2. trvswgnr revised this gist Feb 8, 2025. 1 changed file with 4 additions and 2 deletions.
    6 changes: 4 additions & 2 deletions enum.ts
    Original file line number Diff line number Diff line change
    @@ -49,7 +49,7 @@ type IndexOf<T extends readonly unknown[], V> = T extends [
    : never;

    /** checks if a value is in a tuple */
    type Includes<T extends readonly any[], U> = T extends [
    type Includes<T extends readonly unknown[], U> = T extends [
    infer First,
    ...infer Rest,
    ]
    @@ -118,7 +118,7 @@ function NumericEnum<
    }
    return x;
    }
    type NumericEnum<T extends Record<string, any>> = Extract<T[keyof T], number>;
    type NumericEnum<T extends Record<string, unknown>> = Extract<T[keyof T], number>;

    declare const VARIANT: unique symbol;
    /** a branded type for variants of a string enum */
    @@ -148,6 +148,7 @@ function Enum<const T extends Record<string, string>, const B extends string>(
    name: B,
    t: T,
    ): Prettify<StringEnumReturn<T, B>>;
    // biome-ignore lint/suspicious/noExplicitAny: <explanation>
    function Enum(...args: any[]) {
    if (typeof args[0] === "string" && typeof args[1] === "object") {
    return args[1];
    @@ -176,6 +177,7 @@ type Priority = Enum<typeof Priority>;
    const InvalidPriority1 = Enum("1Low", "Normal", "High");
    // ^?
    type InvalidPriority1 = Enum<typeof InvalidPriority1>;
    // ^?

    // invalid key with special character
    const InvalidPriority2 = Enum("Low", "Normal", "H!gh");
  3. trvswgnr created this gist Feb 8, 2025.
    196 changes: 196 additions & 0 deletions enum.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,196 @@
    declare const TS_ERROR: unique symbol;
    /** used for showing errors in typescript */
    type TSError<Message extends string, Satistifes = unknown> = Satistifes & {
    [TS_ERROR]: Message;
    };

    /** prettifies a type, so that it shows the underlying type instead of the type alias */
    type Prettify<T> = { [K in keyof T]: T[K] } & {};

    /** splits a string into tuple of characters */
    type StringToTuple<S extends string> = S extends `${infer F}${infer R}`
    ? [F, ...StringToTuple<R>]
    : [];

    /** checks if string includes character */
    type StringIncludes<Str extends string, Char extends string> = Includes<
    StringToTuple<Str>,
    Char
    >;

    /** checks if string starts with another string */
    type StartsWith<T extends string, U extends string> = T extends `${U}${string}`
    ? true
    : false;

    // biome-ignore format: long union type would be many lines
    type AlphaLower = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z";
    type AlphaUpper = Uppercase<AlphaLower>;
    type Alpha = AlphaLower | AlphaUpper;

    /** valid starting characters for an enum key */
    type ValidStart = Alpha | "_" | "$";

    /** checks if string starts with an alphabetic character */
    type StartsWithAlpha<T extends string> = StartsWith<T, ValidStart>;

    // biome-ignore format: long union type would be many lines
    /** a non-exhaustive list of special characters that are invalid in enum keys */
    type InvalidSpecialChars = "!" | "@" | "#" | "%" | "^" | "&" | "*" | "(" | ")" | "-" | "=" | "+" | "[" | "]" | "{" | "}" | ";" | ":" | "'" | '"' | "," | "." | "<" | ">" | "/" | "?" | "\\" | "|" | "`" | "~";

    /** gets the index of a value in a tuple */
    type IndexOf<T extends readonly unknown[], V> = T extends [
    ...infer Rest,
    infer Last,
    ]
    ? V extends Last
    ? Rest["length"]
    : IndexOf<Rest, V>
    : never;

    /** checks if a value is in a tuple */
    type Includes<T extends readonly any[], U> = T extends [
    infer First,
    ...infer Rest,
    ]
    ? First extends U
    ? true
    : Includes<Rest, U>
    : false;

    /** validates uniqueness of a tuple */
    type IsUnique<
    T extends readonly unknown[],
    Cache extends readonly unknown[] = [],
    > = T extends [infer First, ...infer Rest]
    ? Includes<Cache, First> extends true
    ? false
    : IsUnique<Rest, [...Cache, First]>
    : true;

    /** validates a key in an enum, returning a custom ts error if invalid */
    type ValidateEnumKey<T extends string> = StartsWithAlpha<T> extends true
    ? StringIncludes<T, InvalidSpecialChars> extends false
    ? T
    : TSError<"Keys must not contain special characters">
    : TSError<"Keys must start with an alphabetic character">;

    /** checks if all keys in a tuple are valid enum keys */
    type AllKeysValid<T extends readonly string[]> = T extends [
    infer Head extends string,
    ...infer Tail extends readonly string[],
    ]
    ? ValidateEnumKey<Head> extends string
    ? AllKeysValid<Tail>
    : false
    : true;

    // biome-ignore format: uglier w/ formatting
    /** checks if all keys in a tuple are unique and valid */
    type UniqueAndValid<K0 extends string, T extends readonly string[]> = IsUnique<[K0, ...T]> extends true
    ? AllKeysValid<[K0, ...T]> extends true
    ? T
    : TSError<
    "One or more keys are invalid. Keys must start with an alphabetic character and not contain special characters",
    readonly string[]
    >
    : TSError<"Keys must be unique", readonly string[]>;

    /** return type for numeric enum function */
    type NumericEnumReturn<
    K0 extends string,
    T extends readonly string[],
    > = UniqueAndValid<K0, T> extends T
    ? { readonly [K in [K0, ...T][number]]: IndexOf<[K0, ...T], K> }
    : never;

    function NumericEnum<
    const K0 extends string,
    const T extends readonly string[],
    >(
    k0: ValidateEnumKey<K0>,
    ...keys: UniqueAndValid<K0, T>
    ): Prettify<NumericEnumReturn<K0, T>> {
    const x = Object.create(null);
    x[k0] = 0;
    for (const k of keys) {
    x[k] = keys.indexOf(k) + 1;
    }
    return x;
    }
    type NumericEnum<T extends Record<string, any>> = Extract<T[keyof T], number>;

    declare const VARIANT: unique symbol;
    /** a branded type for variants of a string enum */
    type Variant<K, T> = T & { [VARIANT]: K };

    /** return type for string enum function */
    type StringEnumReturn<T extends Record<string, string>, B extends string> = {
    [K in keyof T]: Variant<`${B}.${K extends string ? K : never}`, T[K]>;
    };

    function StringEnum<
    const T extends Record<string, string>,
    const B extends string,
    >(_name: B, t: T): StringEnumReturn<T, B> {
    return t as never;
    }
    /** union of all string enum values */
    type StringEnum<T extends Record<string, string>> = Extract<T[keyof T], string>;

    /** creates a numeric enum */
    function Enum<const K0 extends string, const T extends readonly string[]>(
    k0: ValidateEnumKey<K0>,
    ...keys: UniqueAndValid<K0, T>
    ): Prettify<NumericEnumReturn<K0, T>>;
    /** creates a string enum */
    function Enum<const T extends Record<string, string>, const B extends string>(
    name: B,
    t: T,
    ): Prettify<StringEnumReturn<T, B>>;
    function Enum(...args: any[]) {
    if (typeof args[0] === "string" && typeof args[1] === "object") {
    return args[1];
    }
    return NumericEnum(args[0], ...(args.slice(1) as never));
    }
    // biome-ignore lint/suspicious/noRedeclare: intentionally redeclaring Enum
    type Enum<T> = [T] extends [never]
    ? TSError<"Invalid Enum">
    : T extends Record<string, string>
    ? StringEnum<T>
    : T extends Record<string, number>
    ? NumericEnum<T>
    : never;


    // examples

    // happy path
    const Priority = Enum("Low", "Normal", "High");
    // ^?
    type Priority = Enum<typeof Priority>;
    // ^?

    // invalid key starting with number
    const InvalidPriority1 = Enum("1Low", "Normal", "High");
    // ^?
    type InvalidPriority1 = Enum<typeof InvalidPriority1>;

    // invalid key with special character
    const InvalidPriority2 = Enum("Low", "Normal", "H!gh");
    // ^?
    type InvalidPriority2 = Enum<typeof InvalidPriority2>;
    // ^?

    // invalid duplicate key
    const InvalidPriority3 = Enum("Low", "Normal", "Low");
    // ^?
    type InvalidPriority3 = Enum<typeof InvalidPriority3>;
    // ^?


    const Status = Enum("Status", { Open: "o", Closed: "c" });
    // ^?
    type Status = Enum<typeof Status>;
    // ^?