TypeScript
StateMesh is written in TypeScript and provides first-class type support. This guide covers type inference patterns, generic usage, and how to get the most out of TypeScript with StateMesh.
State Type Inference
When you create a mesh, TypeScript infers the state type from the state object:
const mesh = createMesh({
state: {
theme: "light" as "light" | "dark",
user: null as User | null,
cart: {
items: [] as CartItem[],
count: 0
}
}
});
// mesh.getState() returns the inferred type
const state = mesh.getState();
// ^? { theme: "light" | "dark"; user: User | null; cart: { items: CartItem[]; count: number } }You can also provide an explicit type parameter:
interface AppState {
theme: "light" | "dark";
user: User | null;
cart: {
items: CartItem[];
count: number;
};
}
const mesh = createMesh<AppState>({
state: { theme: "light", user: null, cart: { items: [], count: 0 } }
});Type-Safe Paths
setPath and getPath accept dot-separated string paths. TypeScript validates these paths against the state type:
mesh.setPath("theme", "dark"); // ✅
mesh.setPath("cart.count", 5); // ✅
mesh.setPath("cart.items", []); // ✅
mesh.setPath("nonexistent", "value"); // ❌ Type errorThe Path type extracts all valid dot-separated paths from your state type:
import type { Path } from "statemesh-core";
type AppPaths = Path<AppState>;
// "theme" | "user" | "cart" | "cart.items" | "cart.count"Hook Generics
Most hooks infer types from the mesh. When you need explicit types, use generics:
// Inferred from mesh
const [theme, setTheme] = useMeshState("theme");
// Explicit generic (when type can't be inferred)
const [theme, setTheme] = useMeshState<"light" | "dark">("theme");
// Selector with explicit return type
const count = useMeshSelector<number>((state) => state.cart.count);Action Types
Actions infer payload and result types from the handler:
const addItem = mesh.action("cart.addItem", (state, product: Product) => {
state.cart.items.push({ ...product, quantity: 1 });
});
// addItem is typed as (product: Product) => void
addItem({ id: "1", name: "Keyboard", price: 99 }); // ✅
addItem({ id: "1" }); // ❌ Missing fieldsTransaction Types
Transactions infer payload, result, and state types from the definition:
const checkout = mesh.transaction("cart.checkout", {
async effect(state, payload: { method: string }, ctx) {
return { orderId: "123", total: 99 };
},
commit(state, result) {
// result is typed as { orderId: string; total: number }
state.order = result;
}
});
// checkout.run is typed as (payload: { method: string }) => Promise<{ orderId: string; total: number }>
const result = await checkout.run({ method: "card" });Resource Types
Resources infer params and data types from the definition:
const products = mesh.resource("products.list", {
key: (filters: { search: string; page: number }) => ["products", filters],
async fetch(filters, ctx) {
return api.get<Product[]>("/products", { query: filters });
}
});
// products.fetch is typed as (filters: { search: string; page: number }) => Promise<Product[]>Event Types
Events use a discriminated union. TypeScript narrows the event shape based on the type field:
mesh.on({ type: "action.completed" }, (event) => {
// event is narrowed to ActionCompletedEvent
console.log(event.name); // ✅
console.log(event.payload); // ✅
});
mesh.on({ type: "resource.error" }, (event) => {
// event is narrowed to ResourceErrorEvent
console.log(event.error); // ✅
console.log(event.name); // ✅
});Form Types
Forms infer field types from initialValues:
const profileForm = mesh.form("profile.form", {
initialValues: {
name: "",
email: "",
age: 0
},
fields: {
name(value) {
// value is typed as string
return value.trim() ? null : "Name is required";
},
email(value) {
// value is typed as string
return value.includes("@") ? null : "Invalid email";
}
}
});
// useMeshForm infers the form values type
const form = useMeshForm<{ name: string; email: string; age: number }>("profile.form");Error Types
StateMesh exports 16 typed error classes. Each has a code, metadata, and optional cause:
import { TransactionError, ApiClientError, isApiClientError } from "statemesh-core";
try {
await checkout.run();
} catch (error) {
if (error instanceof TransactionError) {
console.log(error.code); // "STATEMESH_TRANSACTION_ERROR"
console.log(error.metadata); // { transaction: "cart.checkout", payload: ... }
}
if (isApiClientError(error)) {
console.log(error.status); // 401
}
}