TypeScript + React Basics
14 exemplos para você começar com TypeScript + React -- 9 básicos e 5 intermediários.
Busque em todas as páginas da documentação
14 exemplos para você começar com TypeScript + React -- 9 básicos e 5 intermediários.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Um projeto React padrão com TypeScript é suficiente. Para Next.js:
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-appConvenções usadas ao longo do documento:
.tsx. Arquivos .ts puros são apenas para módulos não-JSX.strict: true em tsconfig.json -- que é o padrão em novos scaffolds do Next.js e Vite.react (ReactNode, ComponentType, FormEvent, etc.) em vez de declará-los novamente.Novo em TypeScript com React? Veja TypeScript Basics for React para a introdução conceitual a tipos, interfaces e tsconfig.
Defina a estrutura das props de um componente como uma interface TypeScript.
interface GreetingProps {
name: string;
age: number;
}
function Greeting({ name, age }: GreetingProps) {
return (
<p>
{name} is {age}
</p>
);
}<ComponentName>Props por convenção -- fácil de encontrar, fácil de estender.({ name, age }: GreetingProps).interface para props de componentes -- a fusão de declarações facilita a extensão; aliases de type também são aceitáveis.React.FC -- ele adiciona um children implícito que você muitas vezes não quer e está caindo em desuso em 2026.Relacionado: Typing Props -- props opcionais, children, uniões discriminadas | Components (Fundamentals) -- props em profundidade
Marque props como opcionais com ?, depois forneça um valor padrão na desestruturação.
interface ButtonProps {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
}
function Button({ label, variant = "primary", disabled = false }: ButtonProps) {
return (
<button className={variant} disabled={disabled}>
{label}
</button>
);
}variant?: ... torna a prop opcional; TypeScript adiciona undefined ao seu tipo.variant = "primary") preenchem quando o chamador omite a prop."primary" | "secondary") restringe os chamadores a opções válidas -- o autocompletar funciona imediatamente.defaultProps -- ele é depreciado para componentes de função no React 19.Relacionado: Typing Props -- uniões, optional chaining, props de variante | Discriminated Unions -- quando variantes carregam campos diferentes
childrenAceite qualquer conteúdo renderizável com React.ReactNode.
import type { ReactNode } from "react";
interface CardProps {
title: string;
children: ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}ReactNode é o tipo mais abrangente para "renderizável" -- strings, números, elementos, arrays, fragments e null.ReactElement quando você especificamente precisa de um único elemento JSX (por exemplo, para clonagem).type (import type { ReactNode }) -- imports type-only são apagados em tempo de execução, mantendo o bundle limpo.children como uma função: children: (state: T) => ReactNode.Relacionado: Typing Props -- variantes de children (element, function, array) | Composition -- children como o primitivo de composição
useStateDeixe a inferência fazer o trabalho, e dê um tipo explícito quando o valor inicial não for suficiente.
import { useState } from "react";
interface User {
id: string;
name: string;
}
function UserPanel() {
// Inferred as number from the initial value
const [count, setCount] = useState(0);
// Explicit generic needed when initial value does not carry the full type
const [user, setUser] = useState<User | null>(null);
return (
<p>
{count} - {user?.name ?? "no user"}
</p>
);
}useState(initial) tiver informação suficiente -- useState(0) já é number.useState<T>(null) quando o valor puder ser algo mais rico posteriormente -- caso contrário, o TS o fixa em null.User | null tornam o estado "ainda não carregado" explícito -- consumidores devem refinar antes de usar campos.Relacionado: Typing State -- formas de estado complexas, estado discriminado | useState -- o hook subjacente
Use os tipos de evento genéricos que o React exporta para que e.target, e.key e similares permaneçam tipificados.
import type { ChangeEvent, MouseEvent } from "react";
import { useState } from "react";
export default function SearchBar() {
const [q, setQ] = useState("");
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setQ(e.target.value);
};
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log("pressed at", e.clientX, e.clientY);
};
return (
<>
<input value={q} onChange={handleChange} />
<button onClick={handleClick}>Search</button>
</>
);
}ChangeEvent<HTMLInputElement>, MouseEvent<HTMLButtonElement>.KeyboardEvent<HTMLInputElement> e leia e.key.FormEvent<HTMLFormElement> e lembre-se de e.preventDefault().onChange.Relacionado: Typing Events -- todos os tipos de evento, padrões de formulário | Mouse Events / Form Events -- as APIs subjacentes
useRef para um Nó DOMTipifique useRef com o elemento que ele referenciará, inicializado com null.
import { useEffect, useRef } from "react";
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}useRef<HTMLInputElement>(null) fornece { current: HTMLInputElement | null } -- o null corresponde ao próprio tipo do React.inputRef.current?.focus()) -- a ref é null antes que o elemento seja montado.useRef<T | null>(null) e atribua quando necessário.HTMLInputElement, não HTMLElement) para manter .value, .checked, etc. tipificados.Relacionado: Typing Refs -- refs DOM, refs mutáveis,
forwardRef| useRef -- o hook
in, typeof e DiscriminadoresTransforme uma união em um ramo específico para que os campos sejam seguros para acessar.
type Response =
| { kind: "ok"; data: string }
| { kind: "error"; message: string };
function render(res: Response): string {
if (res.kind === "ok") {
// res is now { kind: "ok"; data: string }
return `Got: ${res.data}`;
}
return `Failed: ${res.message}`;
}kind) permite que o TypeScript refine a união com uma única verificação.typeof value === "string" refina primitivos; "field" in obj refina formas de objeto sem um discriminador.Array.isArray(x) e instanceof também funcionam -- o TS sabe que cada um deles refina o tipo.Relacionado: Type Narrowing -- in, typeof, instanceof, funções de asserção | Discriminated Unions -- o padrão que torna o refinamento limpo
Codifique "ou este modo ou aquele modo" para que combinações inválidas de props falhem em tempo de compilação.
type LinkProps =
| { as: "button"; onClick: () => void; href?: never }
| { as: "anchor"; href: string; onClick?: never };
function ActionLink(props: LinkProps) {
if (props.as === "button") {
return <button onClick={props.onClick}>Click</button>;
}
return <a href={props.href}>Click</a>;
}
// <ActionLink as="button" onClick={...} /> ✓
// <ActionLink as="anchor" href="..." /> ✓
// <ActionLink as="anchor" onClick={...} /> ✗ type erroras é o discriminador -- cada valor carrega props diferentes, obrigatórias ou proibidas.never torna um erro de compilação misturar modos.as.Relacionado: Discriminated Unions -- padrões de design, refinamento de tipo | Typing Props -- formas de props avançadas
Derive novos tipos de tipos existentes em vez de escrever duas interfaces que se desalinham.
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
type UserSummary = Pick<User, "id" | "name">;
type NewUser = Omit<User, "id" | "createdAt">;
type UserUpdate = Partial<Omit<User, "id">>;Pick<T, K> mantém apenas as chaves listadas; Omit<T, K> as remove -- componha para expressar formas de API.Partial<T> torna toda propriedade opcional; útil para endpoints PATCH e estado de formulário.Required<T>, Readonly<T> e Record<K, V> completam os utilitários mais usados.Relacionado: Utility Types for React -- todos os utilitários com exemplos React | Typing API Responses -- usando utilitários em limites de API
Permita que o chamador especifique o tipo do item para que o componente permaneça tipificado para qualquer lista.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyOf: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyOf }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyOf(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage - T is inferred from `items`
function UsersPage({ users }: { users: { id: string; name: string }[] }) {
return (
<List
items={users}
keyOf={(u) => u.id}
renderItem={(u) => <span>{u.name}</span>}
/>
);
}function List<T>(...)) e reutilize T nas props.T do primeiro argumento que o carrega -- chamadores raramente escrevem o genérico explicitamente.extends para restringir o genérico: <T extends { id: string | number }> permite usar id sem precisar de keyOf.Relacionado: Generics in React -- hooks genéricos, restrições, forwardRef | Utility Types -- compondo utilitários com genéricos
Compartilhe dados via context sem uma asserção non-null em cada consumidor.
"use client";
import { createContext, useContext, useState, type ReactNode } from "react";
interface AuthValue {
user: { id: string; name: string } | null;
login: (name: string) => void;
}
const AuthCtx = createContext<AuthValue | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthValue["user"]>(null);
const login = (name: string) => setUser({ id: crypto.randomUUID(), name });
return <AuthCtx.Provider value={{ user, login }}>{children}</AuthCtx.Provider>;
}
export function useAuth() {
const ctx = useContext(AuthCtx);
if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
return ctx;
}undefined e envolva useContext em um hook customizado que lança um erro -- consumidores obtêm um valor non-null.useAuth), não o context bruto -- consumidores não podem contornar a verificação.AuthValue["user"] indexa na interface para que o setter permaneça sincronizado com a forma.Relacionado: Typing Context -- providers, contexts divididos, seletores | useContext -- o hook
fetch Tipificado com ZodGaranta em tempo de execução que as respostas da API correspondam ao tipo TypeScript que você envia.
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json: unknown = await res.json();
return UserSchema.parse(json); // validates + narrows to User
}Promise<User> é uma mentira se você nunca validar -- a API pode retornar qualquer coisa.unknown até que ela passe pelo schema -- nenhum any acidental vaza.queryFn + schema do TanStack Query para que cada consulta no app seja tipificada de ponta a ponta.Relacionado: Typing API Responses -- padrões, formas de erro, composição de schema | Zod Basics -- o primer do Zod
O Next.js 15 torna params e searchParams assíncronos -- tipifique-os como Promise<T>.
// app/posts/[id]/page.tsx
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ preview?: string }>;
}
export default async function PostPage({ params, searchParams }: PageProps) {
const { id } = await params;
const { preview } = await searchParams;
return <p>Post {id} {preview ? "(preview)" : ""}</p>;
}// app/api/posts/[id]/route.ts
import { NextResponse } from "next/server";
interface Ctx {
params: Promise<{ id: string }>;
}
export async function GET(_req: Request, ctx: Ctx) {
const { id } = await ctx.params;
return NextResponse.json({ id });
}params e searchParams são Promises -- sempre use await antes de ler as chaves.PageProps / Ctx local por rota; não use um genérico AppPageProps.state e FormData da action para evitar any.Request (padrão Web); retorne NextResponse.json(body) para respostas JSON tipificadas.Relacionado: Typing Server Components -- props, componentes assíncronos, limites | Typing Route Handlers -- assinaturas GET/POST/PUT, middleware
Escreva um .d.ts para adicionar tipos a um pacote JavaScript puro que não envia seus próprios tipos.
// types/colorful-logger.d.ts
declare module "colorful-logger" {
export interface LogOptions {
color?: "red" | "green" | "blue";
bold?: boolean;
}
export function log(message: string, options?: LogOptions): void;
export function clear(): void;
}Então em tsconfig.json:
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
}
}declare module "<name>" informa ao TypeScript a superfície pública de um módulo que não possui tipos embutidos.npm install --save-dev @types/<pkg>) antes de escrever o seu próprio..d.ts ambientais pequenos e com nomes claros; uma declaração feita manualmente que cai em desuso é pior do que any.declare global para globais em tempo de execução (por exemplo, aumentando window); use declarações de módulo para imports.Relacionado: Declaration Files -- declarações de módulo, globais e ambientais | Strict Mode Patterns -- mantendo a honestidade ao integrar dependências não tipificadas
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥