diff --git a/.changeset/sweet-lemons-eat.md b/.changeset/sweet-lemons-eat.md new file mode 100644 index 0000000000..2364a3c173 --- /dev/null +++ b/.changeset/sweet-lemons-eat.md @@ -0,0 +1,30 @@ +--- +"@latticexyz/store": patch +"@latticexyz/world": patch +--- + +`defineStore` and `defineWorld` now maps your `enums` to usable, strongly-typed enums on `enumValues`. + +```ts +const config = defineStore({ + enums: { + TerrainType: ["Water", "Grass", "Sand"], + }, +}); + +config.enumValues.TerrainType.Water; +// ^? (property) Water: 0 + +config.enumValues.TerrainType.Grass; +// ^? (property) Grass: 1 +``` + +This allows for easier referencing of enum values (i.e. `uint8` equivalent) in contract calls. + +```ts +writeContract({ + // … + functionName: "setTerrainType", + args: [config.enumValues.TerrainType.Grass], +}); +``` diff --git a/packages/store/ts/config/v2/enums.ts b/packages/store/ts/config/v2/enums.ts index 69096ef817..fcc447e32b 100644 --- a/packages/store/ts/config/v2/enums.ts +++ b/packages/store/ts/config/v2/enums.ts @@ -1,7 +1,9 @@ -import { Enums } from "./output"; +import { flatMorph } from "@arktype/util"; +import { EnumsInput } from "./input"; import { AbiTypeScope, extendScope } from "./scope"; +import { parseNumber } from "./generics"; -function isEnums(enums: unknown): enums is Enums { +function isEnums(enums: unknown): enums is EnumsInput { return ( typeof enums === "object" && enums != null && @@ -9,9 +11,9 @@ function isEnums(enums: unknown): enums is Enums { ); } -export type scopeWithEnums = Enums extends enums +export type scopeWithEnums = EnumsInput extends enums ? scope - : enums extends Enums + : enums extends EnumsInput ? extendScope : scope; @@ -26,4 +28,23 @@ export function scopeWithEnums return scope as never; } -export type resolveEnums = { readonly [key in keyof enums]: Readonly }; +export type resolveEnums = { + readonly [key in keyof enums]: Readonly; +}; + +export function resolveEnums(enums: enums): resolveEnums { + return enums; +} + +export type mapEnums = { + readonly [key in keyof enums]: { + readonly [element in keyof enums[key] as enums[key][element] & string]: parseNumber; + }; +}; + +export function mapEnums(enums: enums): resolveEnums { + return flatMorph(enums as EnumsInput, (enumName, enumElements) => [ + enumName, + flatMorph(enumElements, (enumIndex, enumElement) => [enumElement, enumIndex]), + ]) as never; +} diff --git a/packages/store/ts/config/v2/generics.ts b/packages/store/ts/config/v2/generics.ts index a269784c59..facfdb3c4e 100644 --- a/packages/store/ts/config/v2/generics.ts +++ b/packages/store/ts/config/v2/generics.ts @@ -51,3 +51,5 @@ export function mergeIfUndefined( allKeys.map((key) => [key, base[key as keyof base] ?? merged[key as keyof merged]]), ) as never; } + +export type parseNumber = T extends `${infer N extends number}` ? N : never; diff --git a/packages/store/ts/config/v2/input.ts b/packages/store/ts/config/v2/input.ts index 07a36188d7..89fa9c0cb8 100644 --- a/packages/store/ts/config/v2/input.ts +++ b/packages/store/ts/config/v2/input.ts @@ -1,8 +1,12 @@ import { Hex } from "viem"; -import { Codegen, Enums, TableCodegen, TableDeploy, UserTypes } from "./output"; +import { Codegen, TableCodegen, TableDeploy, UserTypes } from "./output"; import { Scope } from "./scope"; import { evaluate } from "@arktype/util"; +export type EnumsInput = { + readonly [enumName: string]: readonly [string, ...string[]]; +}; + export type SchemaInput = { readonly [key: string]: string; }; @@ -30,7 +34,7 @@ export type StoreInput = { readonly namespace?: string; readonly tables?: TablesInput; readonly userTypes?: UserTypes; - readonly enums?: Enums; + readonly enums?: EnumsInput; readonly codegen?: Partial; }; diff --git a/packages/store/ts/config/v2/output.ts b/packages/store/ts/config/v2/output.ts index 23e79ad15b..2d13f56677 100644 --- a/packages/store/ts/config/v2/output.ts +++ b/packages/store/ts/config/v2/output.ts @@ -1,14 +1,20 @@ import { evaluate } from "@arktype/util"; import { AbiType, Schema, Table as BaseTable } from "@latticexyz/config"; +import { EnumsInput } from "./input"; export type { AbiType, Schema }; export type UserTypes = { - readonly [userTypeName: string]: { type: AbiType; filePath: string }; + readonly [userTypeName: string]: { + readonly type: AbiType; + readonly filePath: string; + }; }; -export type Enums = { - readonly [enumName: string]: readonly [string, ...string[]]; +export type EnumValues = { + readonly [enumName: string]: { + readonly [enumElement: string]: number; + }; }; export type TableCodegen = { @@ -41,7 +47,8 @@ export type Store = { readonly [namespacedTableName: string]: Table; }; readonly userTypes: UserTypes; - readonly enums: Enums; + readonly enums: EnumsInput; + readonly enumValues: EnumValues; readonly namespace: string; readonly codegen: Codegen; }; diff --git a/packages/store/ts/config/v2/store.test.ts b/packages/store/ts/config/v2/store.test.ts index 0faf3c999e..d89baccf70 100644 --- a/packages/store/ts/config/v2/store.test.ts +++ b/packages/store/ts/config/v2/store.test.ts @@ -44,6 +44,7 @@ describe("defineStore", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", codegen: CODEGEN_DEFAULTS, } as const; @@ -96,6 +97,7 @@ describe("defineStore", () => { dynamic: { type: "string", filePath: "path/to/file" }, }, enums: {}, + enumValues: {}, namespace: "", codegen: CODEGEN_DEFAULTS, } as const; @@ -141,6 +143,7 @@ describe("defineStore", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", codegen: CODEGEN_DEFAULTS, } as const; @@ -213,6 +216,7 @@ describe("defineStore", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", codegen: CODEGEN_DEFAULTS, } as const; @@ -292,6 +296,7 @@ describe("defineStore", () => { Dynamic: { type: "string", filePath: "path/to/file" }, }, enums: {}, + enumValues: {}, namespace: "", codegen: CODEGEN_DEFAULTS, } as const; @@ -403,6 +408,12 @@ describe("defineStore", () => { enums: { ValidNames: ["first", "second"], }, + enumValues: { + ValidNames: { + first: 0, + second: 1, + }, + }, namespace: "", codegen: CODEGEN_DEFAULTS, } as const; @@ -501,7 +512,16 @@ describe("defineStore", () => { Example: ["First", "Second"], } as const; - attest(defineStore({ enums }).enums).equals(enums); + attest(defineStore({ enums }).enums).equals({ + Example: ["First", "Second"], + }); + + attest(defineStore({ enums }).enumValues).equals({ + Example: { + First: 0, + Second: 1, + }, + }); }); it("should allow a const config as input", () => { diff --git a/packages/store/ts/config/v2/store.ts b/packages/store/ts/config/v2/store.ts index 0922998f56..c9312c6093 100644 --- a/packages/store/ts/config/v2/store.ts +++ b/packages/store/ts/config/v2/store.ts @@ -1,11 +1,11 @@ -import { ErrorMessage, flatMorph, narrow } from "@arktype/util"; +import { ErrorMessage, evaluate, flatMorph, narrow } from "@arktype/util"; import { get, hasOwnKey, mergeIfUndefined } from "./generics"; import { UserTypes } from "./output"; import { CONFIG_DEFAULTS } from "./defaults"; import { StoreInput } from "./input"; import { resolveTables, validateTables } from "./tables"; import { scopeWithUserTypes, validateUserTypes } from "./userTypes"; -import { resolveEnums, scopeWithEnums } from "./enums"; +import { mapEnums, resolveEnums, scopeWithEnums } from "./enums"; import { resolveCodegen } from "./codegen"; export type extendedScope = scopeWithEnums, scopeWithUserTypes>>; @@ -56,7 +56,8 @@ export type resolveStore = { > : {}; readonly userTypes: "userTypes" extends keyof store ? store["userTypes"] : {}; - readonly enums: "enums" extends keyof store ? resolveEnums : {}; + readonly enums: "enums" extends keyof store ? evaluate> : {}; + readonly enumValues: "enums" extends keyof store ? evaluate> : {}; readonly namespace: "namespace" extends keyof store ? store["namespace"] : CONFIG_DEFAULTS["namespace"]; readonly codegen: "codegen" extends keyof store ? resolveCodegen : resolveCodegen<{}>; }; @@ -71,7 +72,8 @@ export function resolveStore(store: store): reso extendedScope(store), ), userTypes: store.userTypes ?? {}, - enums: store.enums ?? {}, + enums: resolveEnums(store.enums ?? {}), + enumValues: mapEnums(store.enums ?? {}), namespace: store.namespace ?? CONFIG_DEFAULTS["namespace"], codegen: resolveCodegen(store.codegen), } as never; diff --git a/packages/store/ts/config/v2/storeWithShorthands.test.ts b/packages/store/ts/config/v2/storeWithShorthands.test.ts index c25fd9fc7b..831c3454c8 100644 --- a/packages/store/ts/config/v2/storeWithShorthands.test.ts +++ b/packages/store/ts/config/v2/storeWithShorthands.test.ts @@ -32,6 +32,7 @@ describe("defineStoreWithShorthands", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", codegen: CODEGEN_DEFAULTS, } as const; @@ -68,6 +69,7 @@ describe("defineStoreWithShorthands", () => { }, userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, enums: {}, + enumValues: {}, namespace: "", codegen: CODEGEN_DEFAULTS, } as const; @@ -108,6 +110,7 @@ describe("defineStoreWithShorthands", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", codegen: CODEGEN_DEFAULTS, } as const; @@ -148,6 +151,7 @@ describe("defineStoreWithShorthands", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", codegen: CODEGEN_DEFAULTS, } as const; diff --git a/packages/world/ts/config/v2/world.test.ts b/packages/world/ts/config/v2/world.test.ts index 245e9c9e26..31f91920a3 100644 --- a/packages/world/ts/config/v2/world.test.ts +++ b/packages/world/ts/config/v2/world.test.ts @@ -61,6 +61,7 @@ describe("defineWorld", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", } as const; @@ -128,6 +129,12 @@ describe("defineWorld", () => { enums: { MyEnum: ["First", "Second"], }, + enumValues: { + MyEnum: { + First: 0, + Second: 1, + }, + }, namespace: "", } as const; @@ -227,6 +234,7 @@ describe("defineWorld", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", } as const; @@ -279,6 +287,7 @@ describe("defineWorld", () => { dynamic: { type: "string", filePath: "path/to/file" }, }, enums: {}, + enumValues: {}, namespace: "", } as const; @@ -324,6 +333,7 @@ describe("defineWorld", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", deploy: DEPLOY_DEFAULTS, } as const; @@ -397,6 +407,7 @@ describe("defineWorld", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", } as const; @@ -477,6 +488,7 @@ describe("defineWorld", () => { Dynamic: { type: "string", filePath: "path/to/file" }, }, enums: {}, + enumValues: {}, namespace: "", } as const; @@ -589,6 +601,12 @@ describe("defineWorld", () => { enums: { ValidNames: ["first", "second"], }, + enumValues: { + ValidNames: { + first: 0, + second: 1, + }, + }, namespace: "", } as const; diff --git a/packages/world/ts/config/v2/worldWithShorthands.test.ts b/packages/world/ts/config/v2/worldWithShorthands.test.ts index 0a186d71f0..d7910cce59 100644 --- a/packages/world/ts/config/v2/worldWithShorthands.test.ts +++ b/packages/world/ts/config/v2/worldWithShorthands.test.ts @@ -60,6 +60,12 @@ describe("defineWorldWithShorthands", () => { enums: { MyEnum: ["First", "Second"], }, + enumValues: { + MyEnum: { + First: 0, + Second: 1, + }, + }, namespace: "", } as const; @@ -123,6 +129,12 @@ describe("defineWorldWithShorthands", () => { enums: { MyEnum: ["First", "Second"], }, + enumValues: { + MyEnum: { + First: 0, + Second: 1, + }, + }, namespace: "", } as const; @@ -158,6 +170,7 @@ describe("defineWorldWithShorthands", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", } as const; @@ -196,6 +209,7 @@ describe("defineWorldWithShorthands", () => { }, userTypes: { CustomType: { type: "address", filePath: "path/to/file" as string } }, enums: {}, + enumValues: {}, namespace: "", } as const; @@ -236,6 +250,7 @@ describe("defineWorldWithShorthands", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", } as const; @@ -276,6 +291,7 @@ describe("defineWorldWithShorthands", () => { }, userTypes: {}, enums: {}, + enumValues: {}, namespace: "", } as const;