Skip to content

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:

ts
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:

ts
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:

ts
mesh.setPath("theme", "dark");              // ✅
mesh.setPath("cart.count", 5);              // ✅
mesh.setPath("cart.items", []);             // ✅
mesh.setPath("nonexistent", "value");       // ❌ Type error

The Path type extracts all valid dot-separated paths from your state type:

ts
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:

ts
// 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:

ts
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 fields

Transaction Types

Transactions infer payload, result, and state types from the definition:

ts
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:

ts
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:

ts
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:

ts
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:

ts
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
  }
}

Next Steps

  • State — Deep dive into state reads, writes, and subscriptions
  • Actions — Named mutations with error handling
  • Errors — Full error class reference

Released under the MIT License.