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.
typescript enums that are compatible with the --experimental-strip-types flag introduced in node v22.6.0
// --- 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 & {
[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 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>,
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 | "_" | "$";
// 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;
/** 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> = StartsWith<T, ValidStart> extends true
? StringIncludes<T, InvalidSpecialChars> extends false
? T
: 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 [
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 a letter, underscore (_), or dollar sign ($), and not contain other 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, unknown>> = 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>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment