Um guia denso e direto ao ponto para programadores experientes que querem dominar TypeScript em poucos dias — sem enrolação.
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.
// 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
// 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";
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...) |
// 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"
// 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; }
undefined. São coisas diferentes!
Um valor pode ser de múltiplos tipos. TypeScript força você a tratar cada caso.
Combina múltiplos tipos em um único. O valor precisa satisfazer todos.
TS estreita o tipo dentro de blocos if, switches e type guards.
O tipo é um valor específico, não apenas uma categoria como string.
Funções que verificam o tipo em runtime e informam o TS sobre isso.
// 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 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 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
// 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 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 }
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> );
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: [] });
// 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>) => {};
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>; }
// 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" }; }
// 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 }; }
// 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); }
| Utility | O que faz | Exemplo |
|---|---|---|
Partial<T> | Todas as props opcionais | Partial<Usuario> |
Required<T> | Todas as props obrigatórias | Required<Config> |
Readonly<T> | Todas as props somente leitura | Readonly<Estado> |
Pick<T, K> | Seleciona apenas as keys K | Pick<Usuario, "id" | "nome"> |
Omit<T, K> | Remove as keys K | Omit<Usuario, "senha"> |
Record<K, V> | Objeto com chaves K e valores V | Record<string, number> |
Exclude<T, U> | Remove U de union T | Exclude<"a"|"b"|"c", "a"> |
Extract<T, U> | Mantém o que T e U têm em comum | Extract<"a"|"b", "b"|"c"> |
NonNullable<T> | Remove null e undefined | NonNullable<string | null> |
ReturnType<T> | Tipo de retorno de uma função | ReturnType<typeof fn> |
Parameters<T> | Tuple dos parâmetros | Parameters<typeof fn> |
Awaited<T> | Desembrulha uma Promise | Awaited<Promise<string>> |
// 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"...
// 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
// 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"
// 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; } }
{
"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
}
}
any (se noImplicitAny não estiver ativo). any desliga toda a proteção do TS.
// ❌ 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()));
as SomeTipo é uma mentira para o compilador. Evite ao máximo, especialmente com dados externos.
// ❌ 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
catch (e) tem tipo unknown por padrão (com strict). Você precisa narrowing.
// ❌ 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)); }
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.