# How to Use Zustand with TypeScript for Type‑Safe State Management

> Learn to use Zustand with TypeScript for type-safe state management. Infer state shape automatically for compile-time safety in actions and selectors.

- Repository: [Poimandres/zustand](https://github.com/pmndrs/zustand)
- Tags: how-to-guide
- Published: 2026-03-06

---

**Zustand achieves type‑safe state management by exposing fully generic core types in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) and [`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts) that infer your state shape through the `create<State>()` factory, ensuring compile‑time safety for actions, selectors, and middleware without requiring decorators or interfaces.**

Zustand is a minimal, unopinionated state management library maintained in the pmndrs/zustand repository. Its TypeScript implementation leverages generic type parameters that flow from the vanilla store core through React hooks, allowing you to define your state shape once and have it propagate to every `set`, `get`, and subscription call.

## Core Type Architecture in src/vanilla.ts

The foundation of Zustand’s type system lives in **[`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts)**, which exports the **generic** `StoreApi<T>` interface and the `StateCreator<T, Mis, Mos, U>` type. These definitions ensure that every store instance knows the exact shape of its state at compile time.

The `StoreApi<T>` interface types the three core methods:
- `getState(): T`
- `setState(partial: Partial<T> | (state: T) => Partial<T>): void`
- `subscribe(listener: (state: T, prevState: T) => void): () => void`

Because `T` is generic, calling `store.getState()` returns your specific state type rather than `any` or `unknown`.

### The StateCreator Signature

When you define a store, you pass a **state creator function** to the factory. As implemented in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts), this function receives strongly typed `set` and `get` arguments derived from `StoreApi<T>`:

```typescript
type StateCreator<T, Mis, Mos, U = T> = (
  setState: (fn: (state: T) => Partial<T>) => void,
  getState: () => T,
  store: StoreApi<T>
) => U

```

This means your actions automatically know the state type they read and return, preventing you from accidentally setting non‑existent properties.

## Creating a Type‑Safe React Store

For React projects, **[`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts)** exports the `create` factory. You lock the store to your interface by passing the type parameter before the function call: `create<YourState>()(…)`.

```typescript
import { create } from 'zustand'

interface CounterState {
  count: number
  inc: () => void
  dec: (by: number) => void
}

export const useCounterStore = create<CounterState>()(set => ({
  count: 0,
  inc: () => set(state => ({ count: state.count + 1 })),
  dec: (by) => set(state => ({ count: state.count - by })),
}))

```

**Key points:**
- The generic `<CounterState>` fixes the type for the entire store lifecycle.
- Inside the creator, `set` knows `state` is `CounterState`, so `state.count` is typed as `number`.
- The hook `useCounterStore` returns a selector whose argument is inferred as `CounterState`, giving you autocomplete and compile‑time checking.

Usage in a component preserves these guarantees:

```tsx
function Counter() {
  const { count, inc, dec } = useCounterStore(state => ({
    count: state.count,
    inc: state.inc,
    dec: state.dec,
  }))
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={inc}>+</button>
      <button onClick={() => dec(2)}>-2</button>
    </div>
  )
}

```

Accessing `state.nonExistentProperty` here would raise an immediate TypeScript error because the selector’s parameter is bound to `CounterState`.

## Composing Middleware with Type Preservation

Zustand middleware such as `persist`, `devtools`, and `immer` are implemented as higher‑order functions that wrap your `StateCreator`. Because they accept and return the same generic `StateCreator<T, …>` signature, your type constraints flow through every layer.

```typescript
import { create } from 'zustand'
import { persist, devtools } from 'zustand/middleware'

interface Todo {
  id: string
  text: string
  done: boolean
}

interface TodoState {
  todos: Todo[]
  add: (t: Todo) => void
  toggle: (id: string) => void
}

export const useTodoStore = create<TodoState>()(
  devtools(
    persist(
      set => ({
        todos: [],
        add: (t) => set(state => ({ todos: [...state.todos, t] })),
        toggle: (id) =>
          set(state => ({
            todos: state.todos.map(t =>
              t.id === id ? { ...t, done: !t.done } : t
            ),
          })),
      }),
      { name: 'todo-storage' }
    )
  )
)

```

Even after wrapping the creator in `devtools` and `persist`, the resulting hook still knows that `todos` is an array of `Todo` objects and that `add` expects a `Todo` argument.

## Type‑Safe Vanilla Stores Outside React

Zustand’s vanilla API, also defined in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts), allows you to use the same type system in non‑React environments. Import `createStore` from `zustand/vanilla` to instantiate a store that works in Node.js, testing utilities, or any JavaScript runtime.

```typescript
import { createStore } from 'zustand/vanilla'

interface AuthState {
  user: string | null
  login: (name: string) => void
  logout: () => void
}

export const authStore = createStore<AuthState>(set => ({
  user: null,
  login: name => set({ user: name }),
  logout: () => set({ user: null }),
}))

// Direct usage outside React
authStore.getState().login('alice')
console.log(authStore.getState().user) // "alice"

```

Because `createStore` is generic over `AuthState`, the `getState()` method returns `{ user: string | null; login: …; logout: … }`, and `set` only accepts partial updates matching that interface.

## Summary

- **Generic core:** [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) defines `StoreApi<T>` and `StateCreator<T>` as fully generic, ensuring every store method is typed to your state interface.
- **React integration:** [`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts) exports `create<State>()` which binds the generic parameter to the hook and selector functions.
- **Middleware flow:** Middleware in `src/middleware/*` preserve type safety by accepting and returning the same `StateCreator` generics.
- **Vanilla support:** `createStore<State>` from `zustand/vanilla` provides type‑safe state management outside React with identical inference.

## Frequently Asked Questions

### How do I type the store state in Zustand?

Declare your state shape as a TypeScript interface or type, then pass it as the generic argument to `create<YourState>()`. This locks the `set` and `get` functions to that shape, giving you autocomplete and compile‑time errors for invalid property access.

### Does Zustand middleware break TypeScript inference?

No. Zustand middleware are designed as higher‑order functions that accept and return a `StateCreator` with the same generic parameters. Whether you use `persist`, `devtools`, or `immer`, the original state type flows through the composition unchanged.

### Can I use Zustand types without React?

Yes. Import `createStore` from `zustand/vanilla` to instantiate a store using the same generic types found in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts). This gives you typed `getState()`, `setState()`, and `subscribe()` methods for use in any JavaScript environment.

### How do I type store selectors for better performance?

When calling the hook returned by `create`, pass a selector function that receives your state type as its argument. TypeScript infers the parameter from the store’s generic, ensuring the selector is type‑safe. For strict equality checks, you can also import `useShallow` from `zustand/react/shallow` to prevent unnecessary re‑renders while maintaining type inference.