diff --git a/src/@types/enum-types.ts b/src/@types/enum-types.ts index 4abf36b4226..84df0a96505 100644 --- a/src/@types/enum-types.ts +++ b/src/@types/enum-types.ts @@ -3,7 +3,7 @@ export type EnumOrObject = Record; /** * 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[keyof E]; @@ -12,43 +12,7 @@ export type EnumValues = E[keyof E]; * @example * TSNumericEnum */ -// NB: this works because `EnumValues` returns the underlying Enum union type. export type TSNumericEnum = number extends EnumValues ? T : never; /** Generic type constraint representing a non reverse-mapped TS enum or `const object`. */ export type NormalEnum = Exclude>; - -// ### 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; -testEnumString satisfies NormalEnum; -testObjNum satisfies NormalEnum; -testObjString satisfies NormalEnum; - -testEnumNum satisfies TSNumericEnum; -// @ts-expect-error - This is intentionally supposed to fail as an example -testEnumString satisfies TSNumericEnum; -// @ts-expect-error - This is intentionally supposed to fail as an example -testObjNum satisfies TSNumericEnum; -// @ts-expect-error - This is intentionally supposed to fail as an example -testObjString satisfies TSNumericEnum; diff --git a/src/utils/enums.ts b/src/utils/enums.ts index 1e1e924fe01..7193d014ccc 100644 --- a/src/utils/enums.ts +++ b/src/utils/enums.ts @@ -38,7 +38,7 @@ export function getEnumKeys(enumType: TSNumericEnum): * @remarks * To retrieve the keys of a {@linkcode NormalEnum}, use {@linkcode Object.values} instead. */ -// NB: This does not use `EnumValues` as it messes with variable highlighting in IDEs. +// NB: This intentionally does not use `EnumValues` as using `E[keyof E]` leads to improved variable highlighting in IDEs. export function getEnumValues(enumType: TSNumericEnum): E[keyof E][] { return Object.values(enumType).filter(v => typeof v !== "string") as E[keyof E][]; } @@ -59,7 +59,10 @@ export function getEnumValues(enumType: TSNumericEnum * @remarks * If multiple keys map to the same value, the first one (in insertion order) will be retrieved. */ -export function enumValueToKey(object: NormalEnum, val: EnumValues): keyof T { +export function enumValueToKey>( + object: NormalEnum, + val: V, +): keyof T { for (const [key, value] of Object.entries(object)) { if (val === value) { return key; diff --git a/test/types/enum-types.test-d.ts b/test/types/enum-types.test-d.ts new file mode 100644 index 00000000000..eb1621596bf --- /dev/null +++ b/test/types/enum-types.test-d.ts @@ -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>().toEqualTypeOf(); + expectTypeOf>().branded.toEqualTypeOf<1 | 2>(); + + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toMatchTypeOf<"apple" | "banana">(); + }); + + it("should produce union of const object values as type", () => { + expectTypeOf>().toEqualTypeOf<1 | 2>(); + + expectTypeOf>().toEqualTypeOf<"apple" | "banana">(); + }); + }); + + describe("TSNumericEnum", () => { + it("should match numeric enums", () => { + expectTypeOf>().toEqualTypeOf(); + }); + + it("should not match string enums or const objects", () => { + expectTypeOf>().toBeNever(); + expectTypeOf>().toBeNever(); + expectTypeOf>().toBeNever(); + }); + }); + + describe("NormalEnum", () => { + it("should match string enums or const objects", () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + it("should not match numeric enums", () => { + expectTypeOf>().toBeNever(); + }); + }); + + describe("EnumOrObject", () => { + it("should match any enum or const object", () => { + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + }); + + it("should not match an enum value union w/o typeof", () => { + expectTypeOf().not.toMatchTypeOf(); + expectTypeOf().not.toMatchTypeOf(); + }); + + it("should be equivalent to `TSNumericEnum | NormalEnum`", () => { + expectTypeOf().branded.toEqualTypeOf | NormalEnum>(); + }); + }); +}); + +describe("Enum Functions", () => { + describe("getEnumKeys", () => { + it("should retrieve keys of numeric enum", () => { + expectTypeOf>().returns.toEqualTypeOf<("testN1" | "testN2")[]>(); + }); + }); + + describe("getEnumValues", () => { + it("should retrieve values of numeric enum", () => { + expectTypeOf>().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">(); + }); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index c781bde97ed..14c8f0508eb 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -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",