React: Melhores práticas para TypeScript + React
Última atualização: 2026-08-26
Enquanto a equipe do Tom desenvolvia o módulo de pagamentos, ocorreu um bug grave no ambiente de produção: como um determinado componente esperava
userId, mas recebeustringdo upstream, a solicitação da API falhou. Se o projeto utilizasse TypeScript, essa incompatibilidade de tipos teria sido detectada durante a fase de compilação. Tom decidiu adotar o TypeScript em todo o projeto para detectar esses problemas logo no início.
1. O que você vai aprender
- Definições de tipos de adereços (Estratégias para escolher entre
interfaceetype) - Como funciona o componente genérico
<T> - Sistema de tipos de eventos do React (ChangeEvent / MouseEvent / KeyboardEvent)
- Anotações de tipo completas para ganchos personalizados
- Dicas para ampliar os tipos de atributos dos elementos HTML
2. Diagramas conceituais
O diagrama a seguir ilustra a verificação de tipos realizada pelo TypeScript no fluxo de dados de um componente React:
flowchart LR
subgraph Compilation Phase
A[Parent Component] -->|"Props Type Checking"| B[Child component]
B -->|"State Type Inference"| C[useState]
C -->|"Event Type Validation"| D["onChange / onClick"]
end
subgraph Runtime
E["Actual DOM Event"] --> F["Type Matching"]
F -->|"Through"| G[Execute as usual]
F -->|"Type mismatch"| H[Compilation error]
end
I["interface / type Definition"] --> A
J["Generic Parameters <T>"] --> B
K["React.ChangeEvent"] --> D
style A fill:#e3f2fd,stroke:#1565c0
style B fill:#e8f5e9,stroke:#2e7d32
style D fill:#fff3e0,stroke:#e65100
3. Um cenário da vida real
| Cenário do TypeScript | Problema resolvido | Sintaxe principal |
|---|---|---|
| Definição do tipo de prop | Tipo incorreto passado na chamada | interface Props { name: string } |
| Tipo de evento | Inferência do tipo de parâmetro onChange | e: React.ChangeEvent<HTMLInputElement> |
| Componentes genéricos | Parametrização de tipos de dados para listas/tabelas | <T> Parâmetros genéricos |
| Tipos de hook | Inferência de tipos para useState/useRef | useState<string[]> |
| Tipo de resposta da API | Restrições à estrutura do valor de retorno da interface | interface ApiResponse { data: User[] } |
O projeto de pagamentos do Tom inclui o seguinte fluxo de dados:
Order List Page → PaymentCard Components → AmountInput → Submit API
Sem o TypeScript, AmountInput esperava onSubmit(value: number), mas a página da lista passou onSubmit(value: string), fazendo com que a API recebesse "99.99" em vez de 99.99, o que causou um erro de serialização no back-end.
O TypeScript lida com isso de maneira muito simples — declarando explicitamente os tipos dos parâmetros nas interfaces dos componentes, e qualquer incompatibilidade de tipos resultará em um erro durante a fase npm run build.
(1) Definições de tipos de propriedades de componentes
Existem duas formas principais de definir props no TypeScript: interface e type. As regras para escolher entre elas são as seguintes:
| Método | Cenários aplicáveis | Recursos |
|---|---|---|
interface |
Definindo tipos de objetos: Props / State | A fusão de declarações é suportada, oferecendo bom desempenho |
type |
Tipos de união, tipos de ferramenta, tuplas | Mais flexível, suporta tipos cruzados e tipos condicionais |
Regra geral: Use
interfacepara definir props/estado e usetypepara definir tipos de união/tipos utilitários.
Definições básicas de adereços
interface ButtonProps {
/** Button Text */
label: string
/** Variant Styles */
variant?: 'primary' | 'danger' | 'default'
/** Button Size */
size?: 'small' | 'medium' | 'large'
/** Is it disabled? */
disabled?: boolean
/** Click to Callback */
onClick: () => void
/** Child elements(Icons on buttons, etc.) */
children?: React.ReactNode
}
function Button({
label,
variant = 'primary',
size = 'medium',
disabled = false,
onClick,
children,
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
style={{
padding: size === 'small' ? '4px 12px' : size === 'large' ? '12px 28px' : '8px 20px',
background: variant === 'danger' ? '#ff4d4f' : variant === 'primary' ? '#1890ff' : '#f0f0f0',
color: variant === 'default' ? '#333' : '#fff',
border: 'none',
borderRadius: 6,
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1,
transition: 'all 0.2s',
}}
>
{children}
{label}
</button>
)
}
▶ Exemplo 1: Tipos avançados de propriedades — Estendendo os atributos nativos do HTML
No desenvolvimento prático, muitas vezes é necessário que os componentes recebam atributos HTML nativos (como id, className e aria-*). Você pode usar ComponentPropsWithoutRef para herdá-los:
import { ComponentPropsWithoutRef } from 'react'
// Method 1:Extend Native button Properties(Recommendations)
interface PrimaryButtonProps
extends ComponentPropsWithoutRef<'button'> {
/** Loading Status */
loading?: boolean
/** Icon Name */
icon?: string
}
function PrimaryButton({
loading,
icon,
children,
disabled,
...rest // Remaining Native button Properties
}: PrimaryButtonProps) {
return (
<button
{...rest}
disabled={disabled || loading}
style={{
padding: '8px 24px',
background: loading ? '#91d5ff' : '#1890ff',
color: '#fff',
border: 'none',
borderRadius: 6,
cursor: loading ? 'wait' : 'pointer',
}}
>
{loading ? 'Loading......' : icon ? `${icon} ${children}` : children}
</button>
)
}
// Usage — Both native properties and custom properties can be passed in
<PrimaryButton
id="submit-btn"
loading={isSubmitting}
icon=">"
onClick={() => submit()}
aria-label="Submit Form"
>
Submit
</PrimaryButton>
Ponto-chave: ComponentPropsWithoutRef<'button'> inclui automaticamente todos os atributos nativos do botão, como onClick, disabled, id, className, style e aria-*. Use ...rest para aplicar esses atributos ao elemento botão; não é necessário declará-los individualmente.
(2) Componentes genéricos
Os componentes genéricos permitem que um componente lide com vários tipos de dados, mantendo a segurança de tipos. Os casos de uso mais comuns são os componentes de lista, tabela e seletor.
▶ Exemplo 2: Componente de lista genérico
import { ReactNode } from 'react'
// Generic Interfaces — T For list item types
interface ListProps<T> {
/** Data Sources */
items: T[]
/** Render each item */
renderItem: (item: T, index: number) => ReactNode
/** The Only One key Extract Function */
keyExtractor: (item: T) => string | number
/** Placeholder text when the list is empty */
emptyText?: string
}
// Generic Components — <T,> Grammar(TS for JSX Compatible syntax)
function List<T>({
items,
renderItem,
keyExtractor,
emptyText = 'No data available',
}: ListProps<T>) {
if (items.length === 0) {
return (
<div style={{ textAlign: 'center', padding: 40, color: '#999' }}>
{emptyText}
</div>
)
}
return (
<div>
{items.map((item, index) => (
<div key={keyExtractor(item)} style={{ marginBottom: 8 }}>
{renderItem(item, index)}
</div>
))}
</div>
)
}
// --- Examples of Use ---
interface User {
id: number
name: string
role: 'admin' | 'user'
}
const users: User[] = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Charlie', role: 'user' },
]
// Automatic Type Inference:List<User>
// items Automatically inferred as User[],renderItem 's item Automatically set to User
function UserList() {
return (
<List
items={users}
keyExtractor={user => user.id}
renderItem={(user, index) => (
<div
style={{
padding: '8px 16px',
background: index % 2 === 0 ? '#fafafa' : '#fff',
borderRadius: 4,
}}
>
<span style={{ fontWeight: 600 }}>{user.name}</span>
<span
style={{
marginLeft: 8,
color: user.role === 'admin' ? '#1890ff' : '#999',
fontSize: 12,
}}
>
{user.role}
</span>
</div>
)}
/>
)
}
Principais mecanismos dos genéricos:
TemListProps<T>é um “parâmetro de tipo” que é inferido automaticamente pelo TypeScript quando utilizado.- Quando
items={users}(do tipoUser[]) é passado como parâmetro,itememrenderItempassa automaticamente a ser do tipoUser - O
keyExtractordeitemtambém muda automaticamente paraUser, e chamaruser.idfornece dicas de tipo completas.
(3) Tipos de eventos no React
O React encapsula eventos nativos do DOM usando seu próprio sistema de eventos composto. Para cada tipo de evento, é necessário especificar o tipo de elemento HTML ao qual ele está vinculado, a fim de obter o tipo correto de currentTarget.
| Tipo de evento | Elemento correspondente | Cenários comuns |
|---|---|---|
ChangeEvent<HTMLInputElement> |
input / textarea / select | Campo de formulário |
ChangeEvent<HTMLSelectElement> |
selecionar | menu suspenso |
MouseEvent<HTMLButtonElement> |
botão / div | Clique |
FormEvent<HTMLFormElement> |
formulário | Envio do formulário |
KeyboardEvent<HTMLInputElement> |
entrada | atalho de teclado |
FocusEvent<HTMLInputElement> |
entrada | em foco/fora de foco |
▶ Exemplo 3: Preencha o formulário de pesquisa de tipo de evento
import { useState } from 'react'
interface SearchFormProps {
/** Search Callback */
onSearch: (query: string) => Promise<void>
/** Placeholder text */
placeholder?: string
}
function SearchForm({ onSearch, placeholder = 'Search...' }: SearchFormProps) {
const [query, setQuery] = useState('')
const [isSearching, setIsSearching] = useState(false)
// ChangeEvent<HTMLInputElement> — input Value Changes
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value)
}
// KeyboardEvent<HTMLInputElement> — Keyboard Events
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Escape') {
e.currentTarget.blur() // currentTarget is HTMLInputElement
setQuery('')
}
}
// FormEvent<HTMLFormElement> — Form Submission
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (!query.trim()) return
setIsSearching(true)
try {
await onSearch(query.trim())
} finally {
setIsSearching(false)
}
}
// MouseEvent<HTMLButtonElement> — Click the Clear button
function handleClear(e: React.MouseEvent<HTMLButtonElement>) {
e.stopPropagation() // Prevent Event Bubbling
setQuery('')
}
return (
<form onSubmit={handleSubmit} style={{ display: 'flex', gap: 8 }}>
<div style={{ position: 'relative', flex: 1 }}>
<input
type="text"
value={query}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: 6,
fontSize: 14,
outline: 'none',
boxSizing: 'border-box',
}}
/>
{query && (
<button
type="button"
onClick={handleClear}
style={{
position: 'absolute',
right: 8,
top: '50%',
transform: 'translateY(-50%)',
border: 'none',
background: 'none',
cursor: 'pointer',
color: '#999',
}}
>
x
</button>
)}
</div>
<button
type="submit"
disabled={isSearching || !query.trim()}
style={{
padding: '8px 20px',
background: isSearching ? '#91d5ff' : '#1890ff',
color: '#fff',
border: 'none',
borderRadius: 6,
cursor: isSearching ? 'wait' : 'pointer',
fontSize: 14,
}}
>
{isSearching ? 'Searching......' : 'Search'}
</button>
</form>
)
}
export default SearchForm
Pontos-chave sobre os tipos de eventos:
React.ChangeEvent<HTMLInputElement>— O parâmetro genérico é o tipo do elemento vinculado ao evento, que determina os tipos dee.targetee.currentTargete.currentTargeté o elemento ao qual o evento está vinculado (seguro quanto ao tipo), ee.targeté o elemento que realmente aciona o evento (que pode ser um elemento filho)KeyboardEventparae.keyretorna uma string; não é necessário nenhum tipo adicional
▶ Exemplo 4: Anotações de tipo para hooks personalizados
Os ganchos personalizados também exigem anotações de tipo completas, especialmente ao usar genéricos para permitir que os chamadores especifiquem os tipos de dados:
import { useState, useEffect, useCallback } from 'react'
// Definition Hook Interface for Return Values
interface UseFetchResult<T> {
/** Return Data */
data: T | null
/** Loading... */
loading: boolean
/** Error Message */
error: string | null
/** Manually Resend Request */
refetch: () => void
}
// Generics Hook — Specified by the caller T Type
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchData = useCallback(async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const json: T = await response.json()
setData(json)
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error'
setError(message)
} finally {
setLoading(false)
}
}, [url])
useEffect(() => {
fetchData()
}, [fetchData])
return { data, loading, error, refetch: fetchData }
}
// --- Usage —— Type annotations are easy to understand at a glance ---
interface UserProfile {
id: number
name: string
email: string
avatar: string
}
function ProfilePage({ userId }: { userId: number }) {
// data Automatically inferred as UserProfile | null
const { data: user, loading, error, refetch } =
useFetch<UserProfile>(`/api/users/${userId}`)
if (loading) return <div>Loading......</div>
if (error) return <div style={{ color: 'red' }}>Error:{error}</div>
if (!user) return <div>No data</div>
return (
<div>
<img src={user.avatar} alt={user.name} width={64} />
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={refetch}>Refresh</button>
</div>
)
// ✅ user.name / user.email / user.avatar All have type hints
// ❌ user.phone Compilation errors occur(UserProfile Not included phone)
}
Princípios fundamentais para anotações do tipo hook:
- Defina o valor de retorno como
interfacee adicione comentários JSDoc aos campos (o editor os exibirá automaticamente). useState<T | null>Informar ao TypeScript quedatapode ser nulo e que é necessário verificar se ele é nulo ao ser utilizado- O parâmetro genérico
<T>é passado pelo chamador, euseFetch<UserProfile>faz com que todos os T's dentro deuseFetchse tornemUserProfile
(4) Técnicas avançadas de tipagem: Props condicionais e Omit
Padrão de Props Condicionais
Quando a existência de um prop depende de outro, você pode usar o padrão “união reconhecível”:
// Regular Button vs Link Button — variant as 'link' Must Be Passed On href
type ButtonVariant =
| { variant: 'primary' | 'danger' | 'default' }
| { variant: 'link'; href: string; target?: '_blank' | '_self' }
interface SmartButtonProps {
label: string
} & ButtonVariant
function SmartButton(props: SmartButtonProps) {
if (props.variant === 'link') {
// Here props.href Type-safe existence
return <a href={props.href} target={props.target}>{props.label}</a>
}
return <button>{props.label}</button>
}
Use Omit para omitir propriedades nativas desnecessárias
import { ComponentPropsWithoutRef } from 'react'
// Custom Input Components,Pass-through is not allowed type(Force to text)
type CustomInputProps = Omit<
ComponentPropsWithoutRef<'input'>,
'type'
> & {
label: string
}
function CustomInput({ label, ...inputProps }: CustomInputProps) {
return (
<label>
{label}
<input type="text" {...inputProps} />
</label>
)
}
▶ Exemplo 5: Componentes genéricos — Tabelas de dados com segurança de tipos
interface Column<T> {
key: keyof T & string
title: string
render?: (value: T[keyof T], row: T) => React.ReactNode
}
function DataTable<T extends Record<string, any>>({ data, columns }: { data: T[]; columns: Column<T>[] }) {
return (
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
<thead>
<tr>
{columns.map(col => (
<th key={col.key} style={{ border: '1px solid #ddd', padding: 8, textAlign: 'left', background: '#f5f5f5' }}>
{col.title}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{columns.map(col => (
<td key={col.key} style={{ border: '1px solid #ddd', padding: 8 }}>
{col.render ? col.render(row[col.key], row) : String(row[col.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
interface User {
id: number
name: string
email: string
active: boolean
}
function UserTable() {
const users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@test.com', active: true },
{ id: 2, name: 'Bob', email: 'bob@test.com', active: false },
]
const columns: Column<User>[] = [
{ key: 'name', title: 'Name' },
{ key: 'email', title: 'Email' },
{ key: 'active', title: 'Status', render: (v) => (
<span style={{ color: v ? '#52c41a' : '#999' }}>{v ? 'Active' : 'Inactive'}</span>
)},
]
return <DataTable data={users} columns={columns} />
}
❓ Perguntas Frequentes
P: Como decidir entre
interfaceetype? R: Uma regra simples: Useinterfacepara definir props e state (eles podem ser declarados juntos e oferecem melhor desempenho); usetypepara definir tipos de união, tipos cruzados e tipos utilitários (por exemplo,type Status = 'loading' | 'success' | 'error'). Os dois são intercambiáveis na maioria dos cenários, mas em projetos em equipe, recomenda-se padronizar um deles como escolha principal.
P:
React.FCPor que isso não é mais recomendado? R:React.FC(ouReact.FunctionComponent) inclui a propriedadechildrenpor padrão, mas, na prática, muitos componentes não exigemchildren, o que resulta em uma tipagem excessivamente flexível. Além disso,React.FCnão oferece suporte a componentes genéricos. A melhor prática atual da comunidade é anotar diretamente o tipo dos parâmetros de função e não usar maisReact.FC.
P: Qual é a diferença entre
e.targetee.currentTarget? R:e.currentTargeté o elemento ao qual o evento está vinculado (seguro quanto ao tipo), enquantoe.targeté o elemento que realmente aciona o evento (que pode ser um elemento filho). Por exemplo, seonChangeestiver vinculado a uminput, usee.currentTargetis always thatinput, bute.targetcould be an element inside theinput. In TypeScript, you can usee.currentTargetpara obter o tipo exato do elemento.
P: Como faço para restringir o parâmetro de tipo de um componente genérico de modo a exigir um campo específico? R: Use
extendspara restringir o parâmetro genérico. Por exemplo,<T extends { id: string | number }>garante que T deve incluir um campoid. Se o tipo passado não tiver um campoid, o TypeScript lançará um erro.
P: Ainda devemos usar React.FC? R: A equipe do React não recomenda mais o uso de
React.FC(ou seja,React.FunctionComponent). Os motivos são: ① Ele adiciona implicitamentechildren?: ReactNode, mesmo que seu componente não precise de filhos; ② A sintaxe genérica é complicada (React.FC<Props>em comparação com simplesmente({ prop }: Props) => JSX.Element); ③ A inferência de tipos é menos precisa com exportações padrão do que quando se especificam explicitamente os tipos de parâmetros. Recomenda-se especificar explicitamente os tipos de parâmetros da função:function Comp({ name }: Props) {}.
📖 Resumo
- O tipo
propsé definido por meio deinterface, andComponentPropsWithoutRef, que estende os atributos nativos do HTML - O componente genérico
<T>permite que componentes contêineres, como listas e tabelas, processem vários tipos de dados, mantendo a segurança de tipos. - Os tipos de eventos do React são genéricos:
ChangeEvent<T>,MouseEvent<T>, onde T é o tipo do elemento - Os ganchos personalizados utilizam tipos de retorno genéricos para permitir a inferência de tipos; use
interfacepara definir a estrutura do tipo de retorno - Props (uniões distinguíveis) e
Omitsão técnicas avançadas de tipagem utilizadas para controlar com precisão as interfaces dos componentes
📝 Exercícios
- Crie um componente
Tableusando TypeScript: um<T extends { id: string | number }>genérico que suporte a configuração de colunas (columns:{ key: keyof T, title: string }) e implemente a funcionalidade de classificação. - Crie um hook personalizado
useLocalStorage<T>usando TypeScript: garanta a segurança de tipos durante as operações de leitura e gravação, ofereça suporte a valores padrão e lide automaticamente com a serialização e a desserialização de JSON. - Crie um componente
PasswordInputusandoOmiteComponentPropsWithoutRef: ele herda todas as propriedades de entrada nativas, mastypeé definido como"password", e uma propriedade adicional,showToggle, é adicionada para alternar a visibilidade da senha.