//
Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tipa stores Zustand passando uma interface para create<State>(). Use StateCreator para slices e tipagem de middleware. O suporte TypeScript do Zustand fornece inferência completa para seletores, ações e empilhamento de middleware.
import { create } from "zustand";
// Define estado e ações em uma única interface
interface BearStore {
bears: number;
hungry: boolean;
addBear: () => void;
removeBear: () => void;
setHungry: (hungry: boolean) => void;
}
// Passe a interface como um genérico
const useBearStore = create<BearStore>((set) => ({
bears: 0,
hungry: false,
addBear: () => set((s) => ({ bears: s.bears + 1 })),
removeBear: () => set((s) => ({ bears: Math.max(0, s.bears - 1) })),
setHungry: (hungry) => set({ hungry }),
}));// types/store-types.ts
export interface User {
id: string;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
}
export interface Project {
id: string;
name: string;
ownerId: string;
status: "active" | "archived" | "draft";
}
// Separe estado de ações para clareza
export interface WorkspaceState {
currentUser: User | null;
projects: Project[];
activeProjectId: string | null;
isLoading: boolean;
error: string | null;
}
export interface WorkspaceActions {
setUser: (user: User | null) => void;
fetchProjects: () => Promise<void>;
setActiveProject: (id: string | null) => void;
createProject: (input: Pick<Project, "name">) => Promise<Project>;
archiveProject: (id: string) => Promise<void>;
}
export type WorkspaceStore = WorkspaceState & WorkspaceActions;// stores/workspace-store.ts
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
import type { WorkspaceStore, WorkspaceState, Project } from "@/types/store-types";
const initialState: WorkspaceState = {
currentUser: null,
projects: [],
activeProjectId: null,
isLoading: false,
error: null,
};
export const useWorkspaceStore = create<WorkspaceStore>()(
devtools(
persist(
(set, get) => ({
...initialState,
setUser: (user) => set({ currentUser: user }),
fetchProjects: async () => {
set({ isLoading: true, error: null });
try {
const res = await fetch("/api/projects");
if (!res.ok) throw new Error("Falha ao buscar");
const projects: Project[] = await res.json();
set({ projects, isLoading: false });
} catch (err) {
set({ error: (err as Error).message, isLoading: false });
}
},
setActiveProject: (id) => set({ activeProjectId: id }),
createProject: async (input) => {
set({ isLoading: true, error: null });
try {
const user = get().currentUser;
if (!user) throw new Error("Não autenticado");
const res = await fetch("/api/projects", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...input, ownerId: user.id }),
});
const project: Project = await res.json();
set((s) => ({
projects: [...s.projects, project],
isLoading: false,
}));
return project;
} catch (err) {
set({ error: (err as Error).message, isLoading: false });
throw err;
}
},
archiveProject: async (id) => {
await fetch(`/api/projects/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: "archived" }),
});
set((s) => ({
projects: s.projects.map((p) =>
p.id === id ? { ...p, status: "archived" as const } : p
),
}));
},
}),
{
name: "workspace",
partialize: (state): Pick<WorkspaceState, "activeProjectId"> => ({
activeProjectId: state.activeProjectId,
}),
}
),
{ name: "WorkspaceStore" }
)
);// hooks/use-workspace.ts - hooks de seletor tipados
import { useWorkspaceStore } from "@/stores/workspace-store";
import type { Project } from "@/types/store-types";
export function useActiveProject(): Project | undefined {
return useWorkspaceStore((s) =>
s.projects.find((p) => p.id === s.activeProjectId)
);
}
export function useProjectsByStatus(status: Project["status"]): Project[] {
return useWorkspaceStore((s) => s.projects.filter((p) => p.status === status));
}
export function useIsProjectOwner(projectId: string): boolean {
return useWorkspaceStore(
(s) => s.projects.find((p) => p.id === projectId)?.ownerId === s.currentUser?.id
);
}create<State>() requer o genérico para tipar todo o store (estado + ações).create<State>()(middleware(...)) é necessária para que o TypeScript infira corretamente os tipos de middleware.StateCreator<State, Mutators, Mutators, SliceState> é o tipo principal para padrões de slice e tipagem de middleware.useStore((s: State) => s.field) infere o tipo de retorno do seletor.Extraindo o tipo de estado do store:
// Inferir tipos de um store existente
type State = ReturnType<typeof useWorkspaceStore.getState>;
type ActiveProject = ReturnType<typeof useActiveProject>;StateCreator tipado para slices:
import { StateCreator } from "zustand";
interface AuthSlice {
token: string | null;
login: (credentials: { email: string; password: string }) => Promise<void>;
}
interface DataSlice {
items: string[];
fetchItems: () => Promise<void>;
}
type FullStore = AuthSlice & DataSlice;
const createAuthSlice: StateCreator<FullStore, [], [], AuthSlice> = (set) => ({
token: null,
login: async (credentials) => {
const { token } = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(credentials),
}).then((r) => r.json());
set({ token });
},
});Empilhamento de middleware tipado:
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
// O TypeScript infere o tipo correto através de todas as camadas de middleware
const useStore = create<MyState>()(
devtools(
persist(
immer((set) => ({
// set aceita mutações Draft<MyState> aqui
})),
{ name: "store" }
),
{ name: "DevTools" }
)
);create<State>()() com middleware. Sem a invocação dupla, o TypeScript não consegue inferir os tipos de middleware.set é tipado como (partial: Partial<State> | ((s: State) => Partial<State>), replace?: boolean) => void.get é tipado como () => State, dando acesso completo ao estado atual.immer, o parâmetro de callback de set se torna Draft<State>, permitindo mutações.partialize em persist deve retornar um subconjunto bem tipado: use Pick<State, keys>.// Tipagem explícita para stores complexos
import { StoreApi, UseBoundStore } from "zustand";
type MyStore = UseBoundStore<StoreApi<MyState>>;
const useStore: MyStore = create<MyState>()((set) => ({ ... }));()() com middleware causa erros de TypeScript crípticos. Sempre use create<State>()( middleware(...) ).set({ someField: value }) requer apenas um partial, mas o TypeScript não avisa se você definir um campo que não existe no estado. Ele ignora silenciosamente campos extras.persist, sempre use partialize para excluir ações.StateCreator com quatro parâmetros genéricos para slices, errar a ordem causa erros de tipo confusos. A ordem é: StateCreator<FullStore, MutatorsIn, MutatorsOut, SliceType>.ReturnType<typeof store.getState> inclui funções de ação no tipo. Se você precisar apenas da forma do estado, defina interfaces State e Actions separadas.as const em valores literais em set podem ser necessárias para preservar tipos estreitos (por exemplo, status: "active" as const).| Abordagem | Prós | Contras |
|---|---|---|
| Interface única (Estado + Ações) | Simples, um tipo para gerenciar | Cresce muito para stores complexos |
| Tipos de Estado e Ações separados | Clara separação, tipo de Estado reutilizável | Mais tipos para manter |
| Tipos inferidos (sem genérico explícito) | Menos boilerplate | Garantias mais fracas, fácil de obter any |
| Estado validado por Zod | Validação em tempo de execução + inferência de tipo | Dependência extra, mais código |
interface BearStore {
bears: number;
addBear: () => void;
}
const useBearStore = create<BearStore>((set) => ({
bears: 0,
addBear: () => set((s) => ({ bears: s.bears + 1 })),
}));create<State>().() permite que o TypeScript infira corretamente os tipos de middleware através da cadeia.State e Actions melhoram a clareza e a reutilização.type Store = State & Actions.type State = ReturnType<typeof useWorkspaceStore.getState>;StateCreator é o tipo principal para padrões de slice e middleware.StateCreator<FullStore, MutatorsIn, MutatorsOut, SliceType> é necessária quando um slice precisa de acesso entre slices ou usa middleware.StateCreator<SliceType> é suficiente.useStore((s) => s.count) retorna number se count for tipado como number.set({ nonexistent: 123 }) ignora silenciosamente campos extras.set aceita Partial<State>, que é permissivo com propriedades extras em algumas posições.as const, o TypeScript pode alargar um literal de string como "active" para string.as const para preservar tipos estreitos: status: "active" as const."active" | "archived".partialize: (state): Pick<WorkspaceState, "activeProjectId"> => ({
activeProjectId: state.activeProjectId,
}),Pick<State, keys> para um tipo de retorno explícito e seguro.function useActiveProject(): Project | undefined {
return useWorkspaceStore((s) =>
s.projects.find((p) => p.id === s.activeProjectId)
);
}StoreApi<State> é o tipo de store vanilla (sem hook React).UseBoundStore<StoreApi<State>> é o tipo de hook React retornado por create.StoreApi ao trabalhar com createStore para padrões vanilla ou SSR do Next.js.State e Actions separadas e use State diretamente.Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥