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 recebeu string do 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



2. Diagramas conceituais

O diagrama a seguir ilustra a verificação de tipos realizada pelo TypeScript no fluxo de dados de um componente React:

100%
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:

TEXT 📖 Somente leitura
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 interface para definir props/estado e use type para definir tipos de união/tipos utilitários.

Definições básicas de adereços

TSX
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:

TSX
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>
▶ Experimente

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

TSX 📖 Somente leitura
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>
      )}
    />
  )
}
68 linhas de lógica (limite de 40, somente leitura)

Principais mecanismos dos genéricos:

  1. T em ListProps<T> é um “parâmetro de tipo” que é inferido automaticamente pelo TypeScript quando utilizado.
  2. Quando items={users} (do tipo User[]) é passado como parâmetro, item em renderItem passa automaticamente a ser do tipo User
  3. O keyExtractor de item também muda automaticamente para User, e chamar user.id fornece 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

TSX 📖 Somente leitura
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
88 linhas de lógica (limite de 40, somente leitura)

Pontos-chave sobre os tipos de eventos:

  1. React.ChangeEvent<HTMLInputElement> — O parâmetro genérico é o tipo do elemento vinculado ao evento, que determina os tipos de e.target e e.currentTarget
  2. e.currentTarget é o elemento ao qual o evento está vinculado (seguro quanto ao tipo), e e.target é o elemento que realmente aciona o evento (que pode ser um elemento filho)
  3. KeyboardEvent para e.key retorna 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:

TSX 📖 Somente leitura
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)
}
54 linhas de lógica (limite de 40, somente leitura)

Princípios fundamentais para anotações do tipo hook:

  1. Defina o valor de retorno como interface e adicione comentários JSDoc aos campos (o editor os exibirá automaticamente).
  2. useState<T | null> Informar ao TypeScript que data pode ser nulo e que é necessário verificar se ele é nulo ao ser utilizado
  3. O parâmetro genérico <T> é passado pelo chamador, e useFetch<UserProfile> faz com que todos os T's dentro de useFetch se tornem UserProfile

(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”:

TSX
// 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

TSX
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

TSX 📖 Somente leitura
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} />
}
51 linhas de lógica (limite de 40, somente leitura)

❓ Perguntas Frequentes

P: Como decidir entre interface e type? R: Uma regra simples: Use interface para definir props e state (eles podem ser declarados juntos e oferecem melhor desempenho); use type para 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.FC Por que isso não é mais recomendado? R: React.FC (ou React.FunctionComponent) inclui a propriedade children por padrão, mas, na prática, muitos componentes não exigem children, o que resulta em uma tipagem excessivamente flexível. Além disso, React.FC nã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 mais React.FC.

P: Qual é a diferença entre e.target e e.currentTarget? R: e.currentTarget é o elemento ao qual o evento está vinculado (seguro quanto ao tipo), enquanto e.target é o elemento que realmente aciona o evento (que pode ser um elemento filho). Por exemplo, se onChange estiver vinculado a um input, use e.currentTarget is always that input, but e.target could be an element inside the input. In TypeScript, you can use e.currentTarget para 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 extends para restringir o parâmetro genérico. Por exemplo, <T extends { id: string | number }> garante que T deve incluir um campo id. Se o tipo passado não tiver um campo id, 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 implicitamente children?: 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


📝 Exercícios

  1. Crie um componente Table usando 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.
  2. 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.
  3. Crie um componente PasswordInput usando Omit e ComponentPropsWithoutRef: ele herda todas as propriedades de entrada nativas, mas type é definido como "password", e uma propriedade adicional, showToggle, é adicionada para alternar a visibilidade da senha.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%