This commit is contained in:
Bertie690 2025-06-23 22:31:26 -04:00
parent 903c86a9f6
commit acf57b7a57
4 changed files with 114 additions and 42 deletions

View File

@ -3,7 +3,7 @@ export type EnumOrObject = Record<string | number, string | number>;
/**
* Utility type to extract the enum values from a `const object`,
* or convert an `enum` object produced by `typeof Enum` into the union type representing its values.
* or convert an `enum` interface produced by `typeof Enum` into the union type representing its values.
*/
export type EnumValues<E> = E[keyof E];
@ -12,43 +12,7 @@ export type EnumValues<E> = E[keyof E];
* @example
* TSNumericEnum<typeof WeatherType>
*/
// NB: this works because `EnumValues<typeof Enum>` returns the underlying Enum union type.
export type TSNumericEnum<T extends EnumOrObject> = number extends EnumValues<T> ? T : never;
/** Generic type constraint representing a non reverse-mapped TS enum or `const object`. */
export type NormalEnum<T extends EnumOrObject> = Exclude<T, TSNumericEnum<T>>;
// ### Type check tests
enum testEnumNum {
testN1 = 1,
testN2 = 2,
}
enum testEnumString {
testS1 = "apple",
testS2 = "banana",
}
const testObjNum = { testON1: 1, testON2: 2 } as const;
const testObjString = { testOS1: "apple", testOS2: "banana" } as const;
testEnumNum satisfies EnumOrObject;
testEnumString satisfies EnumOrObject;
testObjNum satisfies EnumOrObject;
testObjString satisfies EnumOrObject;
// @ts-expect-error - This is intentionally supposed to fail as an example
testEnumNum satisfies NormalEnum<typeof testEnumNum>;
testEnumString satisfies NormalEnum<typeof testEnumString>;
testObjNum satisfies NormalEnum<typeof testObjNum>;
testObjString satisfies NormalEnum<typeof testObjString>;
testEnumNum satisfies TSNumericEnum<typeof testEnumNum>;
// @ts-expect-error - This is intentionally supposed to fail as an example
testEnumString satisfies TSNumericEnum<typeof testEnumString>;
// @ts-expect-error - This is intentionally supposed to fail as an example
testObjNum satisfies TSNumericEnum<typeof testObjNum>;
// @ts-expect-error - This is intentionally supposed to fail as an example
testObjString satisfies TSNumericEnum<typeof testObjString>;

View File

@ -38,7 +38,7 @@ export function getEnumKeys<E extends EnumOrObject>(enumType: TSNumericEnum<E>):
* @remarks
* To retrieve the keys of a {@linkcode NormalEnum}, use {@linkcode Object.values} instead.
*/
// NB: This does not use `EnumValues<E>` as it messes with variable highlighting in IDEs.
// NB: This intentionally does not use `EnumValues<E>` as using `E[keyof E]` leads to improved variable highlighting in IDEs.
export function getEnumValues<E extends EnumOrObject>(enumType: TSNumericEnum<E>): E[keyof E][] {
return Object.values(enumType).filter(v => typeof v !== "string") as E[keyof E][];
}
@ -59,7 +59,10 @@ export function getEnumValues<E extends EnumOrObject>(enumType: TSNumericEnum<E>
* @remarks
* If multiple keys map to the same value, the first one (in insertion order) will be retrieved.
*/
export function enumValueToKey<T extends EnumOrObject>(object: NormalEnum<T>, val: EnumValues<T>): keyof T {
export function enumValueToKey<T extends EnumOrObject, V extends EnumValues<T>>(
object: NormalEnum<T>,
val: V,
): keyof T {
for (const [key, value] of Object.entries(object)) {
if (val === value) {
return key;

View File

@ -0,0 +1,101 @@
import type { EnumOrObject, EnumValues, TSNumericEnum, NormalEnum } from "#app/@types/enum-types";
import type { getEnumKeys, getEnumValues } from "#app/utils/enums";
import { enumValueToKey } from "#app/utils/enums";
import { expectTypeOf, describe, it } from "vitest";
enum testEnumNum {
testN1 = 1,
testN2 = 2,
}
enum testEnumString {
testS1 = "apple",
testS2 = "banana",
}
const testObjNum = { testON1: 1, testON2: 2 } as const;
const testObjString = { testOS1: "apple", testOS2: "banana" } as const;
describe("Enum Type Helpers", () => {
describe("EnumValues", () => {
it("should go from enum object type to value type", () => {
expectTypeOf<EnumValues<typeof testEnumNum>>().toEqualTypeOf<testEnumNum>();
expectTypeOf<EnumValues<typeof testEnumNum>>().branded.toEqualTypeOf<1 | 2>();
expectTypeOf<EnumValues<typeof testEnumString>>().toEqualTypeOf<testEnumString>();
expectTypeOf<EnumValues<typeof testEnumString>>().toEqualTypeOf<testEnumString.testS1 | testEnumString.testS2>();
expectTypeOf<EnumValues<typeof testEnumString>>().toMatchTypeOf<"apple" | "banana">();
});
it("should produce union of const object values as type", () => {
expectTypeOf<EnumValues<typeof testObjNum>>().toEqualTypeOf<1 | 2>();
expectTypeOf<EnumValues<typeof testObjString>>().toEqualTypeOf<"apple" | "banana">();
});
});
describe("TSNumericEnum", () => {
it("should match numeric enums", () => {
expectTypeOf<TSNumericEnum<typeof testEnumNum>>().toEqualTypeOf<typeof testEnumNum>();
});
it("should not match string enums or const objects", () => {
expectTypeOf<TSNumericEnum<typeof testEnumString>>().toBeNever();
expectTypeOf<TSNumericEnum<typeof testObjNum>>().toBeNever();
expectTypeOf<TSNumericEnum<typeof testObjString>>().toBeNever();
});
});
describe("NormalEnum", () => {
it("should match string enums or const objects", () => {
expectTypeOf<NormalEnum<typeof testEnumString>>().toEqualTypeOf<typeof testEnumString>();
expectTypeOf<NormalEnum<typeof testObjNum>>().toEqualTypeOf<typeof testObjNum>();
expectTypeOf<NormalEnum<typeof testObjString>>().toEqualTypeOf<typeof testObjString>();
});
it("should not match numeric enums", () => {
expectTypeOf<NormalEnum<typeof testEnumNum>>().toBeNever();
});
});
describe("EnumOrObject", () => {
it("should match any enum or const object", () => {
expectTypeOf<typeof testEnumNum>().toMatchTypeOf<EnumOrObject>();
expectTypeOf<typeof testEnumString>().toMatchTypeOf<EnumOrObject>();
expectTypeOf<typeof testObjNum>().toMatchTypeOf<EnumOrObject>();
expectTypeOf<typeof testObjString>().toMatchTypeOf<EnumOrObject>();
});
it("should not match an enum value union w/o typeof", () => {
expectTypeOf<testEnumNum>().not.toMatchTypeOf<EnumOrObject>();
expectTypeOf<testEnumString>().not.toMatchTypeOf<EnumOrObject>();
});
it("should be equivalent to `TSNumericEnum | NormalEnum`", () => {
expectTypeOf<EnumOrObject>().branded.toEqualTypeOf<TSNumericEnum<EnumOrObject> | NormalEnum<EnumOrObject>>();
});
});
});
describe("Enum Functions", () => {
describe("getEnumKeys", () => {
it("should retrieve keys of numeric enum", () => {
expectTypeOf<typeof getEnumKeys<typeof testEnumNum>>().returns.toEqualTypeOf<("testN1" | "testN2")[]>();
});
});
describe("getEnumValues", () => {
it("should retrieve values of numeric enum", () => {
expectTypeOf<typeof getEnumValues<typeof testEnumNum>>().returns.branded.toEqualTypeOf<(1 | 2)[]>();
});
});
describe("enumValueToKey", () => {
it("should retrieve values for a given key", () => {
// @ts-expect-error oopsie
expectTypeOf(enumValueToKey(testEnumString, testEnumString.testS1)).toEqualTypeOf<"testS1">();
});
});
});

View File

@ -28,19 +28,23 @@ export default defineProject(({ mode }) => ({
}
},
},
environment: "jsdom" as const,
environment: "jsdom",
environmentOptions: {
jsdom: {
resources: "usable",
},
},
typecheck: {
tsconfig: "tsconfig.json",
include: ["./test/types/**/*.{test,spec}{-|.}d.ts"],
},
threads: false,
trace: true,
restoreMocks: true,
watch: false,
coverage: {
provider: "istanbul" as const,
reportsDirectory: "coverage" as const,
provider: "istanbul",
reportsDirectory: "coverage",
reporters: ["text-summary", "html"],
},
name: "main",