Manual de Referência · 2025

TypeScript Para React & Next.js

Um guia denso e direto ao ponto para programadores experientes que querem dominar TypeScript em poucos dias — sem enrolação.

5
Dias
12
Módulos
Exemplos
// Índice
DIA 01
O Sistema de Tipos
Fundamentos que você precisa internalizar antes de qualquer coisa

TypeScript é um superset de JavaScript com tipagem estática. Você já programa — o que muda é que agora o compilador sabe o formato dos seus dados em tempo de desenvolvimento, antes de rodar qualquer linha.

Tipos Primitivos

typescript
// Primitivos — igual ao JS, mas declarados
let nome: string = "Alice";
let idade: number = 30;
let ativo: boolean = true;
let nulo: null = null;
let indefinido: undefined = undefined;
let bignum: bigint = 100n;
let sym: symbol = Symbol("id");

// Inferência: TS descobre o tipo automaticamente
const usuario = "Bob";  // inferred: string
const pontos = 42;    // inferred: number
// dica pro
Prefira sempre deixar o TypeScript inferir o tipo quando possível. Só declare explicitamente quando a inferência não for óbvia ou quando quiser documentar a intenção.

Arrays, Tuples e Enums

typescript
// Arrays — duas sintaxes equivalentes
const nums: number[] = [1, 2, 3];
const strs: Array<string> = ["a", "b"];

// Tuples — arrays com tamanho e tipos fixos
const par: [string, number] = ["idade", 30];
const [chave, valor] = par; // desestruturação tipada

// Enums — cuidado: geram código JS real
enum Direcao { Norte, Sul, Leste, Oeste }
const dir = Direcao.Norte; // 0

// Prefira const enums (sem runtime) ou union de strings:
type Dir = "norte" | "sul" | "leste" | "oeste";

Object Types: Interface vs Type

Esta é uma das perguntas mais comuns. Na prática, são quase equivalentes. A diferença-chave:

Feature interface type
Extension (herança) extends & (intersection)
Declaration merging ✅ Sim ❌ Não
Union types ❌ Não ✅ Sim
Computed types ❌ Não ✅ Sim
Uso recomendado Objetos e APIs públicas Tudo mais (unions, utils...)
typescript
// Interface: preferível para objetos que outros vão "implementar"
interface Usuario {
  id: number;
  nome: string;
  email?: string; // opcional
  readonly criadoEm: Date; // imutável
}

// Extensão de interface
interface Admin extends Usuario {
  permissoes: string[];
}

// Type alias: flexível, bom para unions e computed
type Resultado<T> = { data: T } | { erro: string };
type Chaves = keyof Usuario; // "id" | "nome" | "email" | "criadoEm"

Funções

typescript
// Tipagem explícita de parâmetros e retorno
function somar(a: number, b: number): number {
  return a + b;
}

// Arrow function
const multiplicar = (a: number, b: number): number => a * b;

// Parâmetros opcionais e default
function cumprimentar(nome: string, saudacao = "Olá"): string {
  return `${saudacao}, ${nome}!`;
}

// Rest params
function logger(nivel: string, ...msgs: string[]): void {
  console.log(`[${nivel}]`, ...msgs);
}

// Tipagem de callbacks
type Callback = (erro: Error | null, resultado?: string) => void;

// Overloads — mesmo nome, assinaturas diferentes
function processar(x: string): string;
function processar(x: number): number;
function processar(x: string | number): string | number {
  return typeof x === "string" ? x.toUpperCase() : x * 2;
}
// atenção
never é o tipo de funções que jamais retornam (lançam erro ou loop infinito). void é o retorno de funções que retornam undefined. São coisas diferentes!

DIA 02
Tipos Avançados
O núcleo que separa quem sabe de quem domina
🔀

Union Types

Um valor pode ser de múltiplos tipos. TypeScript força você a tratar cada caso.

⛓️

Intersection Types

Combina múltiplos tipos em um único. O valor precisa satisfazer todos.

🔍

Type Narrowing

TS estreita o tipo dentro de blocos if, switches e type guards.

🏷️

Literal Types

O tipo é um valor específico, não apenas uma categoria como string.

🛡️

Type Guards

Funções que verificam o tipo em runtime e informam o TS sobre isso.

typescript
// Union
type Id = string | number;
function formatarId(id: Id): string {
  if (typeof id === "string") {
    return id.toUpperCase(); // TS sabe: id é string aqui
  }
  return `#${id}`; // TS sabe: id é number aqui
}

// Intersection
type ComTimestamp = { criadoEm: Date; atualizadoEm: Date };
type UsuarioCompleto = Usuario & ComTimestamp;

// Literal types
type Status = "pendente" | "ativo" | "inativo";
type Tamanho = 1 | 2 | 4 | 8;

// Discriminated union — padrão fundamental no React
type Acao =
  | { tipo: "incrementar"; quantidade: number }
  | { tipo: "resetar" }
  | { tipo: "definir"; valor: number };

function reducer(estado: number, acao: Acao): number {
  switch (acao.tipo) {
    case "incrementar": return estado + acao.quantidade;
    case "resetar":     return 0;
    case "definir":     return acao.valor;
  }
}

Type Guards Customizados

typescript
// Type predicate: "se essa função retorna true, TS sabe o tipo"
function isString(x: unknown): x is string {
  return typeof x === "string";
}

// Assertion functions
function assertNonNull<T>(val: T | null | undefined): asserts val is T {
  if (val == null) throw new Error("Valor nulo!");
}

// in operator narrowing
function mover(animal: { voar(): void } | { nadar(): void }) {
  if ("voar" in animal) {
    animal.voar(); // TS sabe que tem voar()
  } else {
    animal.nadar();
  }
}

Mapped Types & Conditional Types

typescript
// Mapped type: transforma cada propriedade
type SoLeitura<T> = { readonly [K in keyof T]: T[K] };
type Parcial<T>  = { [K in keyof T]?: T[K] }; // igual Partial<T> nativo
type Nullable<T> = { [K in keyof T]: T[K] | null };

// Remap de chaves com as
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

// Conditional type: tipo que depende de uma condição
type NonNullable<T> = T extends null | undefined ? never : T;
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// infer: extrai um tipo de dentro de outro
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type Resultado = Unwrap<Promise<string>>; // string

DIA 03
Generics
Escrever código uma vez, funcionar para qualquer tipo
// conceito
Generics são parâmetros de tipo. Assim como funções recebem valores como parâmetros, tipos genéricos recebem outros tipos como parâmetros.
typescript
// Generic básico
function identity<T>(valor: T): T { return valor; }
const num = identity(42);   // T = number, retorna number
const str = identity("hi"); // T = string, retorna string

// Generic com múltiplos parâmetros
function pair<A, B>(a: A, b: B): [A, B] { return [a, b]; }

// Generic com constraints (restrições)
function getLength<T extends { length: number }>(x: T): number {
  return x.length; // só funciona para tipos com .length
}

getLength("hello"); // ✅ string tem .length
getLength([1,2,3]); // ✅ array tem .length

// Generic com default
interface Paginacao<T = unknown> {
  dados: T[];
  pagina: number;
  total: number;
}

// Classe genérica
class Stack<T> {
  private items: T[] = [];
  push(item: T): void { this.items.push(item); }
  pop(): T | undefined  { return this.items.pop(); }
  get size(): number     { return this.items.length; }
}

const pilha = new Stack<number>();
pilha.push(1); pilha.push(2);
const topo = pilha.pop(); // number | undefined

Padrão: Wrapper de API com Generics

typescript
// Padrão que você vai usar em TODO projeto React/Next.js
type ApiResponse<T> =
  | { ok: true; data: T }
  | { ok: false; erro: string; codigo: number };

async function fetchJson<T>(url: string): Promise<ApiResponse<T>> {
  try {
    const res = await fetch(url);
    if (!res.ok) return { ok: false, erro: res.statusText, codigo: res.status };
    const data: T = await res.json();
    return { ok: true, data };
  } catch (e) {
    return { ok: false, erro: String(e), codigo: 0 };
  }
}

// Uso:
const resultado = await fetchJson<Usuario>("/api/usuarios/1");
if (resultado.ok) {
  console.log(resultado.data.nome); // TS sabe que data é Usuario
}

DIA 04
TypeScript com React
Tipando componentes, hooks e eventos do jeito certo

Tipando Props de Componentes

tsx
import type { FC, ReactNode, ComponentProps } from "react";

// Padrão recomendado: type para props
type BotaoProps = {
  children: ReactNode;
  variante?: "primario" | "secundario" | "ghost";
  tamanho?: "sm" | "md" | "lg";
  desabilitado?: boolean;
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
} & Omit<ComponentProps<"button">, "onClick">; // herda attrs HTML!

export function Botao({ children, variante = "primario", ...rest }: BotaoProps) {
  return <button {...rest}>{children}</button>;
}

// FC<Props> é uma alternativa (mas menos explícita sobre o retorno)
const Card: FC<{ titulo: string; corpo: ReactNode }> = ({ titulo, corpo }) => (
  <div><h2>{titulo}</h2><div>{corpo}</div></div>
);

useState, useRef e Hooks Customizados

tsx
import { useState, useRef, useCallback, useReducer } from "react";

// useState — TS infere pelo valor inicial
const [contagem, setContagem] = useState(0); // number
const [usuario, setUsuario] = useState<Usuario | null>(null);

// useRef — dois casos comuns
const inputRef = useRef<HTMLInputElement>(null); // ref de DOM
const valorRef = useRef<number>(0); // valor mutável sem re-render

// Hook customizado com retorno tipado
function useToggle(inicial = false): [boolean, () => void] {
  const [valor, setValor] = useState(inicial);
  const toggle = useCallback(() => setValor(v => !v), []);
  return [valor, toggle];
}

// useReducer tipado
type Estado = { contagem: number; historico: number[] };
type Acao =
  | { tipo: "inc" }
  | { tipo: "dec" }
  | { tipo: "reset"; valor: number };

function reducer(s: Estado, a: Acao): Estado {
  switch (a.tipo) {
    case "inc":   return { contagem: s.contagem + 1, historico: [...s.historico, s.contagem] };
    case "dec":   return { contagem: s.contagem - 1, historico: [...s.historico, s.contagem] };
    case "reset": return { contagem: a.valor, historico: [] };
  }
}

const [estado, dispatch] = useReducer(reducer, { contagem: 0, historico: [] });

Tipando Eventos

tsx
// Referência rápida de tipos de evento mais usados
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {};

Context API Tipada

tsx
import { createContext, useContext, useState } from "react";

type AuthContext = {
  usuario: Usuario | null;
  login(email: string, senha: string): Promise<void>;
  logout(): void;
};

// Trick: inicializar com undefined para forçar uso dentro do Provider
const AuthCtx = createContext<AuthContext | undefined>(undefined);

export function useAuth(): AuthContext {
  const ctx = useContext(AuthCtx);
  if (!ctx) throw new Error("useAuth fora do AuthProvider!");
  return ctx;
}

export function AuthProvider({ children }: { children: ReactNode }) {
  const [usuario, setUsuario] = useState<Usuario | null>(null);

  const value: AuthContext = {
    usuario,
    async login(email, senha) { /* ... */ },
    logout() { setUsuario(null); },
  };

  return <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
}

DIA 05
TypeScript com Next.js
App Router, Server Components, Server Actions e API Routes
// contexto
Next.js 13+ (App Router) introduz Server Components, onde a maioria dos componentes são async por padrão. Isso muda bastante como você tipifica as coisas.

Server Components & Page Props

tsx
// app/usuarios/[id]/page.tsx
type PageProps = {
  params: { id: string };        // sempre string na URL
  searchParams: { [key: string]: string | string[] | undefined };
};

// Server Component (async) — busca dados diretamente
export default async function UsuarioPage({ params, searchParams }: PageProps) {
  const usuario = await buscarUsuario(params.id); // direto no servidor
  if (!usuario) notFound();
  return <PerfilUsuario usuario={usuario} />;
}

// Metadata tipada
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const usuario = await buscarUsuario(params.id);
  return { title: usuario?.nome ?? "Usuário não encontrado" };
}

Server Actions

tsx
// actions/usuario.ts
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod"; // validação em runtime + inferência TS

const CriarUsuarioSchema = z.object({
  nome: z.string().min(2),
  email: z.string().email(),
  idade: z.number().min(18).optional(),
});

// Tipo inferido do schema — única fonte da verdade!
type CriarUsuarioInput = z.infer<typeof CriarUsuarioSchema>;

type ActionResult =
  | { sucesso: true; usuario: Usuario }
  | { sucesso: false; erros: Record<string, string[]> };

export async function criarUsuario(data: CriarUsuarioInput): Promise<ActionResult> {
  const resultado = CriarUsuarioSchema.safeParse(data);
  if (!resultado.success) {
    return { sucesso: false, erros: resultado.error.flatten().fieldErrors };
  }
  const usuario = await db.usuario.create({ data: resultado.data });
  revalidatePath("/usuarios");
  return { sucesso: true, usuario };
}

Route Handlers (API Routes)

tsx
// app/api/usuarios/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";

type RouteContext = { params: { id: string } };

export async function GET(req: NextRequest, { params }: RouteContext) {
  const usuario = await buscarUsuario(params.id);
  if (!usuario) {
    return NextResponse.json({ erro: "não encontrado" }, { status: 404 });
  }
  return NextResponse.json(usuario);
}

export async function PATCH(req: NextRequest, { params }: RouteContext) {
  const body: Partial<Usuario> = await req.json();
  const atualizado = await atualizarUsuario(params.id, body);
  return NextResponse.json(atualizado);
}

REF
Utility Types
Os utilitários nativos que você vai usar toda semana
Utility O que faz Exemplo
Partial<T>Todas as props opcionaisPartial<Usuario>
Required<T>Todas as props obrigatóriasRequired<Config>
Readonly<T>Todas as props somente leituraReadonly<Estado>
Pick<T, K>Seleciona apenas as keys KPick<Usuario, "id" | "nome">
Omit<T, K>Remove as keys KOmit<Usuario, "senha">
Record<K, V>Objeto com chaves K e valores VRecord<string, number>
Exclude<T, U>Remove U de union TExclude<"a"|"b"|"c", "a">
Extract<T, U>Mantém o que T e U têm em comumExtract<"a"|"b", "b"|"c">
NonNullable<T>Remove null e undefinedNonNullable<string | null>
ReturnType<T>Tipo de retorno de uma funçãoReturnType<typeof fn>
Parameters<T>Tuple dos parâmetrosParameters<typeof fn>
Awaited<T>Desembrulha uma PromiseAwaited<Promise<string>>
typescript
// Combinações poderosas do dia a dia
type UsuarioPublico = Omit<Usuario, "senha" | "tokenReset">;
type UpdateUsuario = Partial<Omit<Usuario, "id" | "criadoEm">>;
type UsuarioPreview = Pick<Usuario, "id" | "nome" | "avatar">;

// Extrair tipo do resultado de uma função async
async function getPost(id: string) { /* ... */ }
type Post = Awaited<ReturnType<typeof getPost>>;

// Template literal types
type EventName = `on${Capitalize<string>}`; // "onClick", "onChange"...
type CssVar = `--${string}`;             // "--primary-color"...

REF
Patterns Reais
O que aparece em projetos de produção

Satisfies Operator (TS 4.9+)

typescript
// satisfies valida o tipo MAS mantém o tipo mais específico
type Cor = string | [number, number, number];
type Paleta = Record<string, Cor>;

const paleta = {
  vermelho: [255, 0, 0],
  verde: "#00ff00",
} satisfies Paleta;

// TS sabe que paleta.vermelho é number[], não Cor!
paleta.vermelho.map(x => x * 2); // ✅ não daria com : Paleta

Const Assertions

typescript
// as const: torna o valor literal e imutável
const ROTAS = {
  HOME: "/",
  USUARIOS: "/usuarios",
  ADMIN: "/admin",
} as const;

// Extrair union dos valores
type Rota = (typeof ROTAS)[keyof typeof ROTAS];
// "/"  |  "/usuarios"  |  "/admin"

// Array as const → tuple literal
const PERMISSOES = ["ler", "escrever", "admin"] as const;
type Permissao = (typeof PERMISSOES)[number];
// "ler" | "escrever" | "admin"

Declaration Merging & Module Augmentation

typescript
// Adicionar props ao Session do Next-Auth (next-auth.d.ts)
import "next-auth";

declare module "next-auth" {
  interface Session {
    usuario: {
      id: string;
      papel: "admin" | "usuario";
    };
  }
}

// Estender process.env
declare namespace NodeJS {
  interface ProcessEnv {
    DATABASE_URL: string;
    NEXTAUTH_SECRET: string;
    NEXT_PUBLIC_API_URL: string;
  }
}

CFG
tsconfig.json
O que cada opção importante significa
json
{
  "compilerOptions": {
    // Alvo de compilação
    "target": "ES2022",         // versão JS gerada
    "lib": ["dom", "ES2022"],   // tipos disponíveis
    "module": "ESNext",

    // Rigor — sempre ative no início!
    "strict": true,             // ativa TODOS os checks abaixo:
    "noImplicitAny": true,      // proíbe any implícito
    "strictNullChecks": true,   // null/undefined não são assignáveis a outros tipos
    "noUncheckedIndexedAccess": true, // arr[0] vira T | undefined

    // Qualidade de código
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,

    // Módulos
    "moduleResolution": "bundler",
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],       // alias de importação
      "@ui/*": ["./src/components/ui/*"]
    },

    // Next.js específico
    "jsx": "preserve",
    "plugins": [{ "name": "next" }],
    "incremental": true
  }
}

FIX
Erros Comuns
Os erros que todo mundo comete no começo
// erro #1
any implícito. Quando você não tipar algo e o TS não conseguir inferir, ele vai para any (se noImplicitAny não estiver ativo). any desliga toda a proteção do TS.
typescript
// ❌ Mal — any desliga a tipagem
const dados: any = await fetch("/api").then(r => r.json());
dados.qualquerCoisa; // sem erro = sem segurança

// ✅ Bom — use unknown e narrowing
const dados: unknown = await fetch("/api").then(r => r.json());
if (isUsuario(dados)) {
  dados.nome; // ✅ seguro
}

// ✅ Melhor — use Zod para parse em runtime
const dados = UsuarioSchema.parse(await fetch("/api").then(r => r.json()));
// erro #2
Type assertions abusivas. as SomeTipo é uma mentira para o compilador. Evite ao máximo, especialmente com dados externos.
typescript
// ❌ Mal — você está prometendo algo que pode não ser verdade
const usuario = dados as Usuario;

// ✅ Bom — use type guards ou validação
function isUsuario(x: unknown): x is Usuario {
  return typeof x === "object" && x !== null
    && "id" in x && "nome" in x;
}

// Non-null assertion (!) — use só quando TEM CERTEZA
const el = document.getElementById("app")!; // ok se você sabe que existe
// erro #3
Esquecer de tipar erros em catch. Em TS, catch (e) tem tipo unknown por padrão (com strict). Você precisa narrowing.
typescript
// ❌ Mal
try { ... } catch (e) {
  console.log(e.message); // erro: e é unknown
}

// ✅ Bom
try { ... } catch (e) {
  if (e instanceof Error) {
    console.log(e.message); // seguro
  }
}

// Helper utilitário
function toError(e: unknown): Error {
  return e instanceof Error ? e : new Error(String(e));
}

Checklist de Domínio
Se você sabe fazer tudo aqui, você domina TypeScript para React/Next

📐 Tipos Fundamentais

  • Primitivos e inferência
  • Interface vs Type
  • Union e Intersection
  • Literal types
  • Narrowing e Type Guards

⚙️ Tipos Avançados

  • Generics com constraints
  • Mapped types
  • Conditional types + infer
  • Template literal types
  • satisfies operator

⚛️ React

  • Props com ComponentProps
  • useState/useRef tipados
  • Hooks customizados
  • Context tipado
  • Eventos de formulário

▲ Next.js

  • Page props tipadas
  • Server Actions + Zod
  • Route Handlers
  • Module augmentation
  • tsconfig rigoroso
// próximos passos
Biblioteca recomendada: zod para validação + inferência de tipos de dados externos. @total-typescript/ts-reset para resetar alguns comportamentos confusos do TS nativo. Explore também ts-pattern para pattern matching avançado.