| name | zustand-typescript |
| user-invocable | false |
| description | Use when working with Zustand in TypeScript projects. Covers type-safe store creation, typed selectors, and advanced TypeScript patterns with Zustand. |
| allowed-tools | ["Read","Write","Edit","Bash","Grep","Glob"] |
Zustand - TypeScript Integration
Zustand has excellent TypeScript support out of the box. This skill covers type-safe patterns and best practices for using Zustand with TypeScript.
Key Concepts
Basic Type-Safe Store
Define your store interface and use it with create:
import { create } from 'zustand'
interface BearStore {
bears: number
increasePopulation: () => void
removeAllBears: () => void
updateBears: (newBears: number) => void
}
const useBearStore = create<BearStore>()((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
updateBears: (newBears) => set({ bears: newBears }),
}))
Type Inference
Zustand can infer types automatically:
const useStore = create((set) => ({
count: 0,
text: '',
increment: () => set((state) => ({ count: state.count + 1 })),
setText: (text: string) => set({ text }),
}))
type Store = ReturnType<typeof useStore.getState>
Best Practices
1. Define Store Interfaces
Always define explicit interfaces for better type safety and IDE support:
interface User {
id: string
name: string
email: string
}
interface UserStore {
users: User[]
selectedUserId: string | null
isLoading: boolean
error: string | null
selectedUser: User | null
fetchUsers: () => Promise<void>
selectUser: (id: string) => void
clearSelection: () => void
}
const useUserStore = create<UserStore>()((set, get) => ({
users: [],
selectedUserId: null,
isLoading: false,
error: null,
get selectedUser() {
const { users, selectedUserId } = get()
return users.( u. === selectedUserId) ??
},
: () => {
({ : , : })
{
users = api.()
({ users, : })
} (error) {
({ : error., : })
}
},
: ({ : id }),
: ({ : }),
}))
2. Type-Safe Selectors
Create typed selector functions for reusable logic:
interface TodoStore {
todos: Todo[]
filter: 'all' | 'active' | 'completed'
addTodo: (text: string) => void
toggleTodo: (id: string) => void
setFilter: (filter: TodoStore['filter']) => void
}
const useTodoStore = create<TodoStore>()()
const selectFilteredTodos = (state: TodoStore) => {
if (state.filter === 'all') return state.todos
if (state.filter === 'active') return state.todos.filter((t) => !t.completed)
return state.todos.filter((t) => t.completed)
}
= () =>
state..( !t.).
() {
filteredTodos = (selectFilteredTodos)
activeCount = (selectActiveTodoCount)
(
)
}
3. Slice Pattern with Types
Type-safe store slices for large applications:
import { StateCreator } from 'zustand'
interface BearSlice {
bears: number
addBear: () => void
eatFish: () => void
}
interface FishSlice {
fishes: number
addFish: () => void
}
interface SharedSlice {
addBoth: () => void
getBoth: () => number
}
const createBearSlice: StateCreator<
BearSlice & FishSlice,
[],
[],
BearSlice
> = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})
: <
& ,
[],
[],
> = ({
: ,
: ( ({ : state. + })),
})
: <
& ,
[],
[],
> = ({
: {
().()
().()
},
: (). + ().,
})
useBoundStore = create< & & >()(
({
...(...a),
...(...a),
...(...a),
})
)
4. Generic Store Factory
Create reusable store factories with generics:
import { create, StoreApi } from 'zustand'
interface AsyncState<T> {
data: T | null
isLoading: boolean
error: string | null
}
interface AsyncActions<T> {
fetch: () => Promise<void>
reset: () => void
}
type AsyncStore<T> = AsyncState<T> & AsyncActions<T>
function createAsyncStore<T>(
fetcher: () => Promise<T>
): StoreApi<AsyncStore<T>> {
return create<AsyncStore<T>>()((set) => ({
data: null,
isLoading: false,
error: null,
fetch: async () => {
set({ isLoading: true, error: null })
try {
const data = await fetcher()
({ data, : })
} (error) {
({
: error ? error. : ,
: ,
})
}
},
: ({ : , : , : }),
}))
}
{
:
:
}
useUserStore = createAsyncStore<[]>(
().( r.())
)
5. Type-Safe Middleware
Type middleware correctly for full type safety:
import { create } from 'zustand'
import { persist, devtools } from 'zustand/middleware'
import type { PersistOptions } from 'zustand/middleware'
interface MyStore {
count: number
increment: () => void
}
type MyPersist = (
config: StateCreator<MyStore>,
options: PersistOptions<MyStore>
) => StateCreator<MyStore>
const useStore = create<MyStore>()(
devtools(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{
name: 'my-store',
}
)
)
)
Examples
Type-Safe CRUD Store
interface Entity {
id: string
name: string
createdAt: Date
}
interface CrudStore<T extends Entity> {
items: T[]
selectedId: string | null
isLoading: boolean
error: string | null
selectedItem: T | null
fetchAll: () => Promise<void>
fetchOne: (id: string) => Promise<void>
create: (data: Omit<T, 'id' | 'createdAt'>) => Promise<void>
update: (id: string, data: Partial<T>) => Promise<void>
delete: (: ) => <>
:
}
createCrudStore<T >(
:
): <<T>> {
create<<T>>()( ({
: [],
: ,
: ,
: ,
() {
{ items, selectedId } = ()
items.( item. === selectedId) ??
},
: () => {
({ : , : })
{
response = (apiEndpoint)
items = response.()
({ items, : })
} (error) {
({ : error., : })
}
},
: (id) => {
({ : , : })
{
response = ()
item = response.()
( ({
: state..( i. === id)
? state..( (i. === id ? item : i))
: [...state., item],
: ,
}))
} (error) {
({ : error., : })
}
},
: (data) => {
({ : , : })
{
response = (apiEndpoint, {
: ,
: { : },
: .(data),
})
newItem = response.()
( ({
: [...state., newItem],
: ,
}))
} (error) {
({ : error., : })
}
},
: (id, data) => {
({ : , : })
{
response = (, {
: ,
: { : },
: .(data),
})
updatedItem = response.()
( ({
: state..(
item. === id ? updatedItem : item
),
: ,
}))
} (error) {
({ : error., : })
}
},
: (id) => {
({ : , : })
{
(, { : })
( ({
: state..( item. !== id),
: state. === id ? : state.,
: ,
}))
} (error) {
({ : error., : })
}
},
: ({ : id }),
}))
}
{
:
:
}
useProductStore = createCrudStore<>()
Strongly Typed Actions
Use discriminated unions for type-safe action patterns:
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'set'; value: number }
| { type: 'reset' }
interface CounterStore {
count: number
dispatch: (action: Action) => void
}
const useCounterStore = create<CounterStore>()((set) => ({
count: 0,
dispatch: (action) => {
switch (action.type) {
case 'increment':
set((state) => ({ count: state.count + 1 }))
break
case 'decrement':
set((state) => ({ count: state.count - 1 }))
break
case 'set':
set({ count: action. })
:
({ : })
}
},
}))
dispatch = ( state.)
({ : , : })
({ : , : })
Common Patterns
Namespace Pattern for Large Stores
Organize related state and actions:
interface Store {
auth: {
user: User | null
token: string | null
login: (credentials: Credentials) => Promise<void>
logout: () => void
}
cart: {
items: CartItem[]
addItem: (item: Product) => void
removeItem: (id: string) => void
clear: () => void
}
}
const useStore = create<Store>()((set) => ({
auth: {
user: null,
token: null,
login: async (credentials) => {
const { user, token } = await api.login(credentials)
set((state) => ({
auth: { ...state., user, token },
}))
},
:
( ({
: { ...state., : , : },
})),
},
: {
: [],
:
( ({
: {
...state.,
: [...state.., { ...product, : }],
},
})),
:
( ({
: {
...state.,
: state...( item. !== id),
},
})),
:
( ({
: { ...state., : [] },
})),
},
}))
login = ( state..)
cartItems = ( state..)
Type-Safe Event Emitter
Create a typed event system:
type Events = {
'user:login': { userId: string; timestamp: Date }
'user:logout': { userId: string }
'cart:add': { productId: string; quantity: number }
'cart:remove': { productId: string }
}
type EventListener<T extends keyof Events> = (data: Events[T]) => void
interface EventStore {
listeners: {
[K in keyof Events]?: EventListener<K>[]
}
on: <T extends keyof Events>(event: T, listener: EventListener<T>) => void
off: <T extends keyof Events>(event: T, listener: EventListener<T>) => void
emit: <T extends keyof Events>() =>
}
useEventStore = create<>()( ({
: {},
: {
( ({
: {
...state.,
[event]: [...(state.[event] || []), listener],
},
}))
},
: {
( ({
: {
...state.,
[event]: (state.[event] || []).( l !== listener),
},
}))
},
: {
listeners = ().[event] || []
listeners.( (data))
},
}))
Anti-Patterns
❌ Don't Use any Types
const useStore = create<any>()((set) => ({
data: null,
setData: (data: any) => set({ data }),
}))
interface Store {
data: User | null
setData: (data: User | null) => void
}
const useStore = create<Store>()((set) => ({
data: null,
setData: (data) => set({ data }),
}))
❌ Don't Ignore Return Types
const useStore = create((set) => ({
fetch: async () => {
const data = await api.fetch()
set({ data })
},
}))
interface Store {
fetch: () => Promise<void>
}
const useStore = create<Store>()((set) => ({
fetch: async (): Promise<void> => {
const data = await api.fetch()
set({ data })
},
}))
❌ Don't Mix State and Actions in Types
interface Store {
count: number
increment: () => void
name: string
setName: (name: string) => void
}
interface StoreState {
count: number
name: string
}
interface StoreActions {
increment: () => void
setName: (name: string) => void
}
type Store = StoreState & StoreActions
Related Skills
- zustand-store-patterns: Basic store creation and usage
- zustand-middleware: Using middleware with TypeScript
- zustand-advanced-patterns: Advanced TypeScript patterns and techniques