Client types
A client type is a Rust type of the game logic that the client also reads, such as the type a map holds. Define it once in Rust. Boardweaver gives the client a zod schema and a TypeScript type for it, as boardweaver/game.
Defining client types
Define each client type inside boardweaver_sdk::client_types!:
use boardweaver_sdk::serde::{Deserialize, Serialize};
boardweaver_sdk::client_types! {
#[derive(Clone, Copy, Default, Serialize, Deserialize)]
#[serde(crate = "boardweaver_sdk::serde", default)]
pub struct Tally {
pub played: u32,
pub taken: u32,
}
}
The macro writes each type as you wrote it, so the game logic uses it as any other type. Every type inside it is a client type: there is no list to keep. Call it as often as you like, in any file.
- Write structs and enums only, none generic.
- Derive serde's
Serializeby that name. A writtenimpl Serializecould write any JSON, so the macro refuses it. - Give each field one of these types:
bool, an integer up to 64 bits,f32,f64,String,char,(),Option,Box,Vec,BTreeSet, an array, a tuple of up to six, aBTreeMapkeyed byString, an integer or a client enum without data,serde_json::Value, or another client type. - Mark a field
#[client(any)]when its type is none of these. The client then reads that field as any JSON value.
The serde attributes decide the JSON, as they do in the state, so rename_all, rename, skip, skip_serializing_if, default, transparent and the enum tags all apply.
Importing them in the client
Import a client type from boardweaver/game. Each type gives two names:
| Name | Is |
|---|---|
The Rust name with its first letter lowercased, such as tally |
the zod schema |
The Rust name, such as Tally |
the TypeScript type of the JSON the type writes |
Parse a map with the schema before you read it. A map that is empty is left out of the board, so give the client a value for a missing map:
import { tally, type Tally } from "boardweaver/game";
import { useBoard } from "boardweaver/react";
const NO_TALLY: Tally = { played: 0, taken: 0 };
export default function App() {
const board = useBoard();
if (!board) return null;
return (
<ul>
{board.players.map(({ playerId, publicState }) => {
const { played, taken } = publicState ? tally.parse(publicState) : NO_TALLY;
return <li key={playerId}>{played} played, {taken} taken</li>;
})}
</ul>
);
}
parse throws when the JSON does not fit the type. Use safeParse where a map may hold something else.
The client may also import z and ZodError from zod, as Client libraries lists.
The JSON each type becomes
| Rust | TypeScript |
|---|---|
| an integer | number, checked to the type's range. A 64-bit integer past 2^53 loses precision in JavaScript. |
f32, f64 |
number | null. serde writes NaN and infinity as null. |
Option<T> |
T | null, a key that is always present. With skip_serializing_if = "Option::is_none" the key may be missing instead. |
Vec<T>, BTreeSet<T>, an array |
an array. A set's items are unique, and an array has its exact length. |
| a tuple | a tuple, element by element |
BTreeMap<K, V> |
an object of V. An integer key becomes a string of digits. A client enum key becomes one optional key per variant. |
| a unit enum | its variant names as strings |
| an enum with data | the union of what serde writes for each variant, by its tag |
serde_json::Value, #[client(any)] |
any JSON value |
A type that refers to itself, directly or through other client types, parses in full, but its TypeScript type is any. Write that type yourself when you need it.
When boardweaver/game changes
Each commit and each validate_code builds the client against the client types of the game logic it builds in the same call. validate_code also type-checks the client against them, so a client that reads a field a type does not have gets a type error (Testing a game). A commit that leaves the Rust and data files unchanged keeps the client types of the commit before it.
When the game logic does not build, validate_code builds the client against the client types of the last commit instead, so an import of a client type you added since then is an error too, and it skips the type check (Testing a game).
The module is the file /game.generated.ts in the commit. Boardweaver writes it, so you never edit it. A game that defines no client types has an empty boardweaver/game.
Errors
| Error | Fix |
|---|---|
A compile error from client_types! |
The macro refuses what it cannot describe exactly, such as serde(flatten) or a generic type, and its message says what to write instead. Follow it, or mark the field #[client(any)]. |
bw::client-types |
See Builds, limits and errors. |
The client types ... would both be exported from boardweaver/game as ... |
Two types differ only in the case of their first letter. Rename one. |
The client type ... would be exported from boardweaver/game as ... |
The type's lowercased name is z or a JavaScript reserved word, such as Default. Rename the type. |
The client type ... cannot be exported from boardweaver/game as a TypeScript type |
The type's name is a built-in TypeScript type, such as string. Rename the type. |
No matching export in "shim:boardweaver/game" for import "..." |
The client imports a name no client type gives. Check the name, and that the game logic builds. |
Type error: ...: Property '...' does not exist on type '...' |
The client reads a field the client type does not have. Check the Rust type and its serde attributes, such as rename_all. |
Boardweaver could not write boardweaver/game from the client types: ... |
Boardweaver failed to convert the client types. Report it with file_ticket, and include the client types the game defines. |