//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tipa los stores de Zustand pasando una interfaz a create<State>(). Usa StateCreator para slices y el tipado de middleware. El soporte de TypeScript de Zustand ofrece inferencia completa para selectores, acciones y apilamiento de middleware.
import { create } from "zustand";
// Define state y acciones en una sola interfaz
interface BearStore {
bears: number;
hungry: boolean;
addBear: () => void;
removeBear: () => void;
setHungry: (hungry: boolean) => void;
}
// Pasa la interfaz como 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";
}
// Separa state y acciones para mayor claridad
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("Error al obtener");
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("No 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 selector 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>() requiere el genérico para tipar todo el store (state + acciones).create<State>()(middleware(...)) es necesaria para que TypeScript infiera correctamente los tipos del middleware.StateCreator<State, Mutators, Mutators, SliceState> es el tipo central para patrones de slices y middleware.useStore((s: State) => s.field) infiere el tipo de retorno a partir del selector.Extraer el tipo de state del store:
// Infiere tipos a partir de un 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 });
},
});Apilamiento de middleware tipado:
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
// TypeScript infiere el tipo correcto a través de todas las capas de middleware
const useStore = create<MyState>()(
devtools(
persist(
immer((set) => ({
// set acepta mutaciones Draft<MyState> aquí
})),
{ name: "store" }
),
{ name: "DevTools" }
)
);create<State>()() con middleware. Sin la doble invocación, TypeScript no puede inferir los tipos del middleware.set está tipado como (partial: Partial<State> | ((s: State) => Partial<State>), replace?: boolean) => void.get está tipado como () => State, lo que da acceso completo al state actual.immer, el parámetro callback de set se convierte en Draft<State>, lo que permite mutaciones.partialize en persist debe devolver un subconjunto bien tipado: usa Pick<State, keys>.// Tipado explícito para stores complejos
import { StoreApi, UseBoundStore } from "zustand";
type MyStore = UseBoundStore<StoreApi<MyState>>;
const useStore: MyStore = create<MyState>()((set) => ({ ... }));()() con middleware provoca errores crípticos de TypeScript. Usa siempre create<State>()( middleware(...) ).set({ someField: value }) solo requiere un parcial, pero TypeScript no avisa si estableces un campo que no existe en el state. Ignora silenciosamente los campos extra.persist, usa siempre partialize para excluir las acciones.StateCreator con cuatro parámetros genéricos para slices, un orden incorrecto provoca errores de tipos confusos. El orden es: StateCreator<FullStore, MutatorsIn, MutatorsOut, SliceType>.ReturnType<typeof store.getState> incluye funciones de acción en el tipo. Si solo necesitas la forma del state, define interfaces State y Actions por separado.as const en valores literales dentro de set pueden ser necesarias para preservar tipos estrechos (p. ej., status: "active" as const).| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Interfaz única (State + Actions) | Simple, un solo tipo que gestionar | Crece mucho en stores complejos |
| Tipos State y Actions separados | Separación clara, tipo State reutilizable | Más tipos que mantener |
| Tipos inferidos (sin genérico explícito) | Menos boilerplate | Garantías más débiles, fácil acabar con any |
| State validado con Zod | Validación en runtime + inferencia de tipos | Dependencia extra, más 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 TypeScript infiera correctamente los tipos del middleware a través de la cadena.State y Actions separadas mejoran la claridad y la reutilización.type Store = State & Actions.type State = ReturnType<typeof useWorkspaceStore.getState>;StateCreator es el tipo central para patrones de slices y middleware.StateCreator<FullStore, MutatorsIn, MutatorsOut, SliceType> es necesaria cuando un slice necesita acceso entre slices o usa middleware.StateCreator<SliceType> es suficiente.useStore((s) => s.count) devuelve number si count está tipado como number.set({ nonexistent: 123 }) ignora silenciosamente los campos extra.set acepta Partial<State>, que es permisivo con propiedades extra en algunas posiciones.as const, TypeScript puede ampliar un literal de cadena como "active" a string.as const para preservar tipos estrechos: status: "active" as const."active" | "archived".partialize: (state): Pick<WorkspaceState, "activeProjectId"> => ({
activeProjectId: state.activeProjectId,
}),Pick<State, keys> para un tipo de retorno explícito y seguro en tipos.function useActiveProject(): Project | undefined {
return useWorkspaceStore((s) =>
s.projects.find((p) => p.id === s.activeProjectId)
);
}StoreApi<State> es el tipo de store vanilla (sin hook de React).UseBoundStore<StoreApi<State>> es el tipo de hook de React que devuelve create.StoreApi cuando trabajes con createStore para patrones vanilla o SSR de Next.js.State y Actions por separado y usa State directamente.Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥