Skip to content
D
Documentation

Type stores and middleware

how-to
3 min readUpdated

Use this pattern to type state and actions, infer them from initial values, reuse the store's type, and compose middleware without losing inference. The React example renders a bear count; clicking Increase by 1 updates that count.

1. Annotate state and actions with curried create

In your React TypeScript project, pass your state-and-actions type to create using create<BearState>()(...). The extra () lets you annotate the state while TypeScript infers the middleware type parameters. Your store is a hook: select the state and actions each component needs.

ts
import { create } from 'zustand'

export type BearState = {
  bears: number
  increase: (by: number) => void
}

export const useBearStore = create<BearState>()((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
}))

TypeScript checks that the creator returns every field in BearState. It infers by as number, so you do not need to annotate the action parameter again.

Render the hook's selected values in a component. BearSummary uses the same state type for its props rather than repeating the type of bears.

tsx
import { useBearStore, type BearState } from './store'

function BearSummary({ bears }: Pick<BearState, 'bears'>) {
  return <p>{bears} bears around</p>
}

export default function App() {
  const bears = useBearStore((state) => state.bears)
  const increase = useBearStore((state) => state.increase)

  return (
    <main style={{ minHeight: 200 }}>
      <BearSummary bears={bears} />
      <button onClick={() => increase(1)}>Increase by 1</button>
    </main>
  )
}

Keep the HTML entry point and main.tsx from React quick start; replace its store.ts and App.tsx with the files above to reuse BearState for component props in the mounted counter.

Here, BearState also supplies the component prop type; see State and actions for the bound hook and its store methods.

2. Infer state with combine and extract its type

Replace store.ts with the following file; keep App.tsx and main.tsx unchanged. combine merges the initial state with the object returned by its additional state creator. It returns a state creator, not a store, so pass it to create. Use ExtractState to export the inferred state-and-actions type.

ts
import { create, type ExtractState } from 'zustand'
import { combine } from 'zustand/middleware'

export const useBearStore = create(
  combine({ bears: 0 }, (set) => ({
    increase: (by: number) => set((state) => ({ bears: state.bears + by })),
  })),
)

export type BearState = ExtractState<typeof useBearStore>

Use the non-curried create(...) here: combine creates the state, so TypeScript can infer its type. Annotate by because the initial state does not describe that action's parameter.

ExtractState extracts the full state-and-actions type from a store's getState() method. Here, BearState contains both bears: number and increase: (by: number) => void. The existing BearSummary props still compile, and the mounted counter behaves the same way. You can reuse this extracted type in component props, utilities, and tests.

Keep merging updates unless you intend to replace everything

Passing true as the replacement flag replaces the entire state, including actions. Supply a complete state when replacing, or use the default merging update as the examples do.

Be especially careful inside combine: its internal get type describes only the initial state, although the runtime state also includes the returned actions. A replacement containing only bears can compile there while deleting increase. Similarly, Object.keys(get()) includes action keys despite the narrower type. The resulting hook's getState() type includes the full state and actions.

3. Compose middleware directly inside create

Replace store.ts again to add persist and devtools. Keep the middleware nested immediately inside curried create so contextual inference works. Do not move the composition into an untyped wrapper function.

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

export type BearState = {
  bears: number
  increase: (by: number) => void
}

export const useBearStore = create<BearState>()(
  devtools(
    persist(
      (set) => ({
        bears: 0,
        increase: (by) => set((state) => ({ bears: state.bears + by })),
      }),
      {
        name: 'typed-bear-storage',
        partialize: (state) => ({ bears: state.bears }),
      },
    ),
    { name: 'Typed bears' },
  ),
)

The same mounted component now saves the bear count to browser localStorage. With storage available, refreshing the page restores the saved count. partialize selects only bears for storage; TypeScript infers that persisted shape while the hook retains the full BearState type. Install the Redux DevTools extension to inspect the store's updates.

Keep devtools outermost when composing it with middleware such as Immer. It adds a type parameter to setState; middleware that also modifies setState can otherwise lose that parameter.

If you split the creator into slices, apply middleware to the combined store, not inside individual slices. Middleware inside slices can produce unexpected issues. See Split a store into slices for that pattern.

Options that matter here

For a different JSON-backed storage, use the shipped createJSONStorage adapter. It returns PersistStorage or undefined if obtaining storage throws. See Persist store data for storage configuration and hydration behavior.

OptionTypeDefaultWhat it does
persist → namestringRequiredSets the storage key; use a unique name.
persist → partialize(state: S) => PersistedState(state) => stateSelects the value to persist; the return value determines the persisted type.
persist → storagePersistStorage<PersistedState, PersistReturn> | undefinedcreateJSONStorage(() => window.localStorage)Chooses the storage used for persistence.
devtools → namestringNot specifiedNames the DevTools connection.

If you extract a named creator, annotate it with StateCreator, not a hand-written function type. Its generic parameters describe the full state, incoming middleware mutators, outgoing mutators, and the returned slice. A plain StateCreator<BearState> describes a creator without middleware mutators; keep middleware-specific mutator information when annotating creators that depend on it.

Try the live Zustand counter demo to see the hook-and-action interaction. The examples above follow the React starter's separate count and action selectors, with curried creation and middleware typing added here.

Was this page helpful?