Pular para o conteúdo
TypeScript

Tipar Context Provider no React + TS 2026

Context sem tipagem vira terra de ninguém. Aprenda o padrão completo: createContext + Provider + hook customizado, tudo tipado com TypeScript.

Por que isso é importante

Tipar Context Provider no React + TS 2026. Context sem tipagem vira terra de ninguém. Aprenda o padrão completo: createContext + Provider + hook customizado, tudo tipado com TypeScript.

O padrão completo: createContext + Provider + hook

Galera, o segredo de um context bem tipado é seguir um padrão de 3 partes. Primeiro, o createContext com o tipo certo. Segundo, o Provider que encapsula a lógica. Terceiro, um hook customizado que consome o context e já garante que o valor não é undefined.

Esse padrão elimina o problema clássico de fazer useContext e receber undefined porque esqueceu de envolver com Provider. O hook customizado trata isso e dispara erro claro.

Passo a passo: context tipado do zero

  1. Defina a interface do contexto — Liste tudo que o context compartilha: estados, funções, dispatch. Cada item com tipo explícito.
  2. Crie o context com createContext — Passe o tipo como generic e use undefined como valor inicial (sem valor default fake).
  3. Crie o Provider como componente — Ele recebe children, gerencia o estado e passa os valores pelo context.
  4. Crie um hook customizado — O useAuth(), useTheme(), useCart() que faz useContext() internamente e valida se o Provider existe.
  5. Exporte apenas o Provider e o hook — O context em si fica privado. Componentes usam só o hook, nunca o context direto.

Exemplo completo: AuthContext tipado

Vamos criar um contexto de autenticação completo: estado do user, funções de login/logout, loading. Tudo tipado.

import {
  createContext,
  useContext,
  useState,
  useCallback,
  ReactNode,
} from "react";

// 1. Interface do usuário
interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user";
}

// 2. Interface do contexto
interface AuthContextType {
  user: User | null;
  loading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  isAuthenticated: boolean;
}

// 3. Criar context com undefined (sem valor fake)
const AuthContext = createContext<AuthContextType | undefined>(
  undefined
);

// 4. Provider
interface AuthProviderProps {
  children: ReactNode;
}

export function AuthProvider({ children }: AuthProviderProps) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);

  const login = useCallback(
    async (email: string, password: string) => {
      setLoading(true);
      try {
        const res = await fetch("/api/login", {
          method: "POST",
          body: JSON.stringify({ email, password }),
        });
        const data: User = await res.json();
        setUser(data);
      } finally {
        setLoading(false);
      }
    },
    []
  );

  const logout = useCallback(() => {
    setUser(null);
  }, []);

  return (
    <AuthContext.Provider
      value={{
        user,
        loading,
        login,
        logout,
        isAuthenticated: user !== null,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

// 5. Hook customizado (a estrela do padrão)
export function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth deve ser usado dentro de AuthProvider");
  }
  return context;
}

Quem chama useAuth() recebe user, login, logout, loading e isAuthenticated — tudo tipado. Se esquecer de envolver com AuthProvider, o erro aparece na hora, claro e direto.

Context com useReducer: dispatch tipado

Pra contextos mais complexos, useReducer é melhor que useState. E com TypeScript, dá pra tipar cada action com discriminated union — o dispatch só aceita actions válidas.

// State tipado
interface CartState {
  items: CartItem[];
  total: number;
}

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

// Actions com discriminated union
type CartAction =
  | { type: "ADD_ITEM"; payload: CartItem }
  | { type: "REMOVE_ITEM"; payload: { id: string } }
  | { type: "UPDATE_QUANTITY"; payload: { id: string; quantity: number } }
  | { type: "CLEAR_CART" };

// Reducer tipado
function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case "ADD_ITEM":
      return {
        ...state,
        items: [...state.items, action.payload],
        total: state.total + action.payload.price,
      };
    case "REMOVE_ITEM":
      return {
        ...state,
        items: state.items.filter((i) => i.id !== action.payload.id),
      };
    case "CLEAR_CART":
      return { items: [], total: 0 };
    default:
      return state;
  }
}

// Context com dispatch tipado
interface CartContextType {
  state: CartState;
  dispatch: React.Dispatch<CartAction>;
}

const CartContext = createContext<CartContextType | undefined>(
  undefined
);

export function CartProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(cartReducer, {
    items: [],
    total: 0,
  });

  return (
    <CartContext.Provider value={{ state, dispatch }}>
      {children}
    </CartContext.Provider>
  );
}

export function useCart(): CartContextType {
  const context = useContext(CartContext);
  if (!context) throw new Error("useCart fora do CartProvider");
  return context;
}

// Uso:
const { state, dispatch } = useCart();
dispatch({ type: "ADD_ITEM", payload: item });
// TypeScript garante que payload casa com o type

Se alguém tentar dispatch({ type: 'ADD_ITEM' }) sem o payload, o TypeScript barra na hora. Cada action tem a estrutura correta garantida em compile-time.

Context genérico reutilizável

Dá pra criar uma factory de context que funciona com qualquer tipo. Isso evita repetir o boilerplate de createContext + Provider + hook em todo contexto novo.

function createSafeContext<T>(name: string) {
  const Context = createContext<T | undefined>(undefined);

  function useContextSafe(): T {
    const value = useContext(Context);
    if (value === undefined) {
      throw new Error(
        `use${name} deve ser usado dentro de ${name}Provider`
      );
    }
    return value;
  }

  return [Context.Provider, useContextSafe] as const;
}

// Uso:
const [ThemeProvider, useTheme] =
  createSafeContext<ThemeContextType>("Theme");

const [NotificationProvider, useNotification] =
  createSafeContext<NotificationContextType>("Notification");

Com essa factory, criar um novo context tipado leva uma linha. O boilerplate de verificação e error handling fica encapsulado.

Erros comuns ao tipar Context

Passar valor default fake no createContext: createContext({} as AuthContextType) esconde o erro de Provider ausente. Use undefined + validação no hook.

Não criar hook customizado: usar useContext(AuthContext) direto espalha a verificação de undefined pelo app inteiro.

Expor o context diretamente: só exporte o Provider e o hook. O objeto de context fica privado no arquivo.

Não tipar actions do reducer: dispatch sem tipo aceita qualquer objeto, e bugs aparecem só em runtime.

Colocar lógica pesada no Provider: chamadas de API e cálculos complexos dentro do Provider re-renderizam todos os consumers. Use useMemo e useCallback.

Checklist: Context Provider tipado

Checklist Final

  • Interface do contexto criada com todos os valores e funções
  • createContext com generic e undefined como valor inicial
  • Provider como componente com children: ReactNode
  • Hook customizado com throw Error se context é undefined
  • Actions do reducer tipadas com discriminated union
  • Dispatch tipado como React.Dispatch
  • Context exporta apenas Provider e hook (context privado)
  • useMemo/useCallback no Provider pra evitar re-renders
  • Factory genérica criada pra reduzir boilerplate

Context profissional: aplique em projeto real

Context tipado é peça-chave em qualquer app React profissional. No CrazyStack, você implementa contextos reais — Auth, Cart, Theme, Notification — todos com TypeScript, dentro de um projeto completo com Node.js e React.