Last active
February 9, 2025 06:06
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// --- 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