This episode covers writing typed stores with create<State>()(...), defining state and action interfaces, typing middleware, using StoreApi, and the combine pattern for slicing large stores without losing types.

Zustand is written in TypeScript and provides excellent end-to-end typing. Episode 7 covers how to write typed stores: the curried create<State>()(...) pattern you must use, defining state and action interfaces, typing middleware, and combine for composing large stores from several parts.
With correct typing, errors in the store are caught at compile time instead of at runtime. This is the shield you need in a real codebase.
In TypeScript, the create call is written in a curried form so type inference works correctly:
import { create } from 'zustand'
interface CounterState {
count: number
increment: () => void
}
export const useCounter = create<CounterState>()((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}))Notice the double parentheses: create<CounterState>()((set) => ...). The first parentheses receive the generic, the second receive the initializer. Without the double parentheses, TypeScript often fails to infer action types.
Define the entire shape of the state in a single interface:
interface UserState {
user: User | null
status: 'idle' | 'loading' | 'success' | 'error'
fetchUser: (id: string) => Promise<void>
logout: () => void
}interface UserState covers data and functions. Zustand will make sure the object returned by the initializer exactly matches the interface — missing properties or wrongly typed ones are rejected by the compiler immediately.
Selectors are now correctly inferred:
const count = useCounter((s) => s.count) // number
const increment = useCounter((s) => s.increment) // () => void
increment()useCounter((s) => s.count) returns number, and useCounter((s) => s.increment) returns a function. Your editor will give you autocomplete and clear errors when a selector or action is misused.
Middleware has its own typing. The curried pattern still applies:
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface ThemeState {
theme: 'light' | 'dark'
toggle: () => void
}
export const useTheme = create<ThemeState>()(
persist(
(set) => ({
theme: 'light',
toggle: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
}),
{ name: 'theme-storage' },
),
)persist(...) wraps the initializer, and create<ThemeState>()(...) still guarantees the shape of the state. Middleware typing like immer and devtools follows the same pattern.
The StoreApi type describes the full store API: getState, setState, subscribe, and getInitialState. It's useful for typing functions that accept a store as an argument:
import type { StoreApi } from 'zustand'
function resetStore(store: StoreApi<CounterState>) {
store.setState({ count: 0 })
}StoreApi<CounterState> lets the resetStore function accept any store with the same type. This pattern is common in helpers and test utilities.
combine merges several state slices while automatically inferring the combined type:
import { create } from 'zustand'
import { combine } from 'zustand/middleware'
export const useDashboard = create(
combine({ user: null as User | null, theme: 'light' as 'light' | 'dark' },
(set) => ({
login: (user: User) => set({ user }),
toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
})),
)combine(initialState, (set) => ...) merges data state and actions, and TypeScript infers the combined type without writing a manual interface. The choice between combine and an explicit interface is a team preference — for large slicing we use the Slice Pattern in episode 16.
Make sure tsconfig.json uses strict mode:
{
"compilerOptions": {
"strict": true
}
}strict: true in tsconfig activates all strict checks. With this, types like unknown from an error in catch can't be used directly without an assertion.
Episode 7 makes your stores type-safe: the curried create<State>()(...) pattern, state and action interfaces, middleware typing, StoreApi for helpers, and combine for slicing without writing manual types.
Key takeaways:
create<State>()(...) with double parentheses in TypeScript.StoreApi<T> types function parameters that receive a store.In the next episode we dive into the persist middleware — saving state to localStorage automatically, the name and partialize options for choosing the stored subset, and skipHydration for full control of the rehydration process. Your state will survive after the browser is closed.