Skip to content
This page has been auto-translated and may contain errors.View in English

TypeScript no React

Um pai renomeia uma prop de word para currentWord. Três filhos continuam lendo word, e nada avisa: o app compila, a página carrega, e o primeiro usuário que clica no botão que renderiza um desses filhos vê uma tela em branco. TypeScript adiciona uma camada de tipos sobre o JavaScript: cada valor tem um tipo declarado ou inferido, e o compilador marca o código que não combina antes de ele rodar.

No React isso compensa exatamente onde bugs como esse se escondem, nas emendas onde os dados transitam entre componentes. Uma prop tipada é um contrato: o pai deve enviar a forma certa, o filho pode confiar no que chega, e seu editor autocompleta os dois lados.

Este capítulo cobre as partes específicas do React: tipificar state, props, children, function props e valores de retorno de componentes. Assume que você conhece o básico de TypeScript, unions, tipos customizados e genéricos; a seção abre com aulas de refresco em exatamente esses tópicos, e depois coloca tudo em prática reconstruindo o jogo Assembly: Endgame em TypeScript.

Configurando: template react-ts do Vite

O Vite oferece uma versão TypeScript do seu template React. Um flag no scaffolding te dá um projeto totalmente configurado:

bash
npm create vite@latest my-react-app -- --template react-ts

Arquivos de componente usam a extensão .tsx em vez de .jsx (TypeScript mais JSX), e módulos puros sem JSX usam .ts em vez de .js. Tudo mais sobre a configuração funciona do jeito que sempre funcionou; o Vite compila os tipos e envia JavaScript puro para o navegador.

Uma pegadinha vem com isso: erros de tipo aparecem em seu editor e falham npm run build, que roda tsc -b antes de empacotar, mas o servidor de desenvolvimento remove as anotações sem verificá-las. npm run dev vai servir um projeto cheio de erros de tipo, então um app que roda não prova que os tipos passam.

Tipificando state

useState normalmente se tipifica sozinho. O hook infere seu tipo do valor inicial que você passa, então state criado com um valor de início real não precisa de anotação nenhuma:

tsx
const [currentWord, setCurrentWord] = useState(getRandomWord())

Se getRandomWord retorna uma string, currentWord é uma string e setCurrentWord só aceita strings. Chame setCurrentWord(true) e TypeScript marca no ato: um boolean não é atribuível onde se espera uma string. Essa é a inferência fazendo seu trabalho, e para a maioria do state você pode deixar como está.

A inferência quebra quando o valor inicial está vazio ou é solto demais para descrever o futuro do state. Um array vazio diz nada sobre o que vai conter, então useState([]) infere never[], um array cujos elementos nunca podem ser nada, o que torna cada push nele um erro. Aqui você chama a forma explícita: useState é uma função genérica, e você passa o argumento de tipo entre chaves.

tsx
const [guessedLetters, setGuessedLetters] = useState<string[]>([])

Agora o state é um array de strings mesmo que o valor inicial seja um array vazio, e tanto o valor quanto o setter o garantem. O mesmo movimento trata state nullable, onde o valor começa como null e depois vira algo real:

tsx
type Word = { text: string; difficulty: number }

const [selectedWord, setSelectedWord] = useState<Word | null>(null)

A union Word | null diz ao TypeScript ambas as formas que esse state pode legalmente conter. Cada leitura de selectedWord agora te força a lidar com o caso null antes de tocar .text, o que transforma um crash em tempo de execução clássico em um aviso em tempo de compilação. A mecânica do state em si não muda; TypeScript apenas fixa o que o state pode conter.

Tipificando props de componentes

Props chegam como um objeto, então tipificá-las significa anotar esse objeto. Para um componente com uma ou duas props, uma anotação inline funciona:

tsx
function ConfettiContainer({ isGameWon }: { isGameWon: boolean }) {
  // ...
}

Tipos inline ficam confusos conforme as props se multiplicam. O padrão padrão é um type alias nomeado, convencionalmente chamado ComponentNameProps, declarado acima do componente:

tsx
type GameStatusProps = {
  isGameWon: boolean
  wrongGuessCount: number
  message?: string
}

function GameStatus({ isGameWon, wrongGuessCount, message }: GameStatusProps) {
  // ...
}

O ? em message marca como opcional: pais podem omiti-la, e dentro do componente seu tipo é string | undefined. Uma declaração interface GameStatusProps { ... } funciona na mesma posição; para tipificar props os dois são intercambiáveis, e o curso usa type aliases por consistência com os tipos customizados que constrói em outro lugar.

Se um componente aceita children, tipifique como ReactNode, o tipo amplo que cobre tudo que React pode renderizar: elementos, strings, números, fragments, arrays de tudo isso.

tsx
import { type ReactNode } from 'react'

type CardProps = {
  title: string
  children: ReactNode
}

Como children fluem através de um componente é abordado em Children e composição; ReactNode é o tipo que os descreve. Tudo sobre como props se comportam em tempo de execução fica igual. A anotação de tipo adiciona o contrato em cima.

Tipificando function props

Componentes frequentemente recebem funções como props: um click handler, um callback que reporta um valor para cima. O tipo para uma function prop usa sintaxe arrow: a lista de parâmetros com seus tipos, uma arrow, e o tipo de retorno.

tsx
type NewGameButtonProps = {
  startNewGame: () => void
}

type LetterButtonProps = {
  letter: string
  onGuess: (value: string) => void
}

() => void descreve uma função que não toma argumentos e não retorna nada, a forma da maioria dos handlers estilo evento. (value: string) => void diz que o filho vai chamar a função com uma string, então a implementação do pai deve aceitar uma. Passe um handler cujos tipos de parâmetro não combinam e o erro aparece no site da chamada JSX, no pai, em tempo de compilação.

A verificação faz duas concessões, porém, e ambas parecem o type checker deixando algo passar. Um handler pode declarar menos parâmetros que o tipo lista, então onGuess={() => setOpen(true)} satisfaz (value: string) => void. E () => void aceita uma função que retorna um valor, então startNewGame={async () => { await saveScore() }} type-checks mesmo que passe uma promise que ninguém espera. O compilador captura tipos de parâmetro errados em vez de toda diferença de forma.

Tipos de retorno e valores derivados

Passe o mouse sobre um componente de função e TypeScript já sabe que retorna React.JSX.Element, inferido do JSX na declaração de retorno. (O namespace JSX vive dentro do módulo react agora em vez de ser global, então escrever a anotação à mão significa importá-lo.)

Você pode deixar essa inferência em paz, e muitos codebases fazem. Anotar o tipo de retorno é uma escolha sobre rigor: garante que o componente sempre retorna um único elemento, o que alguns times valorizam em codebases maiores. Essa é uma promessa mais rigorosa que React faz, já que um componente pode legalmente retornar uma string, um número, um array, ou null, e uma anotação JSX.Element pura rejeita tudo isso até você expandir.

tsx
import { type JSX } from 'react'

function Header(): JSX.Element {
  return <h1>Assembly: Endgame</h1>
}

function ConfettiContainer({ isGameWon }: { isGameWon: boolean }): JSX.Element | null {
  if (!isGameWon) return null
  return <Confetti />
}

ConfettiContainer às vezes renderiza nada retornando null, o mesmo movimento usado em renderização condicional, então seu tipo de retorno anotado é a union JSX.Element | null. O mesmo hábito se estende a valores derivados dentro do componente: o tipo de retorno de uma arrow function entra depois dos parênteses de parâmetro, e um map sobre dados produzindo JSX rende um JSX.Element[]. A maioria dessas anotações reafirma o que a inferência já concluiu, então trate-as como documentação opcional. As que consistentemente ganham seu lugar estão em limites: props, state vazio ou nullable, e funções cujas assinaturas outros componentes dependem.

Importando tipos compartilhados

Tipos são exportados e importados como qualquer outro binding, o que mantém uma definição servindo todo o app. Módulos de dados comumente exportam sua forma ao lado dos dados:

tsx
// languages.ts
export type Language = {
  name: string
  backgroundColor: string
  color: string
}

// LanguageChips.tsx
import { type Language } from './languages'

type LanguageChipsProps = {
  languages: Language[]
}

A palavra-chave type na importação a marca como type-only, então desaparece completamente do JavaScript compilado. Uma vez que um tipo como Language vive em um lugar, cada componente que toca esses dados importa o mesmo contrato, e mudar a forma em um arquivo expõe cada site de chamada que precisa atualizar.

Objetos de evento são onde a tipificação bate primeiro. Um handler inline consegue seu tipo de evento de graça, porque React sabe o que um <input> passa para onChange, então event em onChange={event => setGuess(event.target.value)} já é tipado. Extraia esse handler em uma função nomeada e o contexto desaparece: o parâmetro vira um any implícito, e a verificação de tipo rigorosa do template transforma isso em um erro. Anote com o tipo de evento React que combina com o elemento.

tsx
function GuessInput() {
  const [guess, setGuess] = useState('')

  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    setGuess(event.target.value)
  }

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setGuess('')
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={guess} onChange={handleChange} />
    </form>
  )
}

O tipo de elemento entre as chaves é o que torna event.target.value uma string; coloque HTMLElement lá em vez e target não carrega value nenhum. A mesma forma cobre o resto: React.MouseEvent<HTMLButtonElement> para clicks, React.KeyboardEvent<HTMLInputElement> para pressionamentos de tecla. Cada um também é importável por nome, como em import { type ChangeEvent } from 'react'. O que os handlers fazem em tempo de execução é território de events e forms; a anotação só nomeia o que React passa para eles.

Código antigo de React + TypeScript tipifica componentes como const Header: React.FC = () => .... React.FC anota a função inteira em vez de seu valor de retorno, e caiu em desuso por razões concretas: historicamente adicionava silenciosamente children às props de todo componente quer aceitasse ou não, e complica componentes genéricos. A convenção atual é a que este capítulo usa, uma função pura com props tipadas e um retorno JSX.Element inferido ou anotado.

Prefira derivar tipos em vez de redeclará-los. Quando um componente precisa de parte de uma forma existente, utility types mantêm a fonte de verdade única intacta: um objeto de inline style do chip que carrega os campos de cor é Pick<Language, 'backgroundColor' | 'color'>. Pick é uma whitelist, então um campo adicionado a Language depois não pode cair no objeto que você passa para style. Recorra a Omit no trabalho oposto, removendo uma chave conhecida de um tipo props, onde herdar qualquer coisa que a fonte tipifique cresça é o comportamento que você quer.

O mesmo instinto se aplica a wrappers DOM-pesados: ComponentProps<'button'> do React puxa cada prop que um button nativo aceita, então um design-system button pode estendê-lo em vez de listar à mão onClick, disabled, e amigos.

Cada política de anotação falha do seu próprio jeito, o que é a parte que vale a pena planejar. Anote tudo e toda refatoração arrasta um diff através de arquivos que apenas reafirmam o que a inferência já conhecia, então essas anotações ficam atrás do código e começam a descrever uma forma que ele não tem mais. Anote apenas as emendas e uma má inferência, um any vazando de uma dependência não tipada, se espalha silenciosamente através de valores locais até encontrar um limite que o rejeita.

O segundo fracasso é mais barato de pegar, porque o limite é onde a anotação já está, então faça anotar-as-emendas o padrão e adicione uma anotação local no momento que o tipo inferido de um valor derivado te surpreenda.

JunoTipifique as emendas, infira o resto TypeScript é como etiquetar as caixas que você passa entre componentes.

State criado com um valor de início real se etiqueta a si mesmo, e quando o valor de início é vazio ou null você escreve a etiqueta você mesmo, como useState<string[]>([]). Props ganham um pequeno tipo nomeado como GameStatusProps listando cada prop e seu tipo.

Uma vez que as etiquetas estão lá, seu editor te avisa no instante que algo da forma errada é passado, antes você até rodar o app.

JunoTipifique as emendas, infira o resto Deixe useState inferir de valores iniciais reais e passe um genérico para os casos vazio e nullable, como useState<Word | null>(null).

Dê a cada componente um alias ComponentNameProps, marque opcionais com ?, tipifique children como ReactNode, e escreva function props como assinaturas como (value: string) => void. Um handler de evento extraído precisa seu parâmetro anotado, como em React.ChangeEvent<HTMLInputElement>, porque só handlers inline conseguem esse tipo de graça.

Compartilhe tipos exportando-os do módulo que possui o dado e importando com import { type Language }.

JunoTipifique as emendas, infira o resto Anote a superfície pública, props e assinaturas exportadas, e deixe a inferência lidar com valores derivados privados.

Pule React.FC em favor de funções puras com props tipadas e um retorno JSX.Element ou JSX.Element | null onde você quer a garantia. Derive em vez de duplicar: Pick e ComponentProps<'button'> mantêm uma fonte de verdade única, então mudanças de forma aparecem como erros de compilação em cada site de chamada afetado.

Esse é o manual. O que começou como uma função retornando um pouco de markup é agora um toolkit completo: componentes e state, effects e dados, padrões de componente reutilizáveis, roteamento, um modelo de renderização para raciocinar sobre performance, e uma camada de tipos que captura erros nas emendas antes que mais alguém os encontre. Escolha um projeto que você realmente quer que exista, scaffolde com npm create vite@latest, e abra esses capítulos de novo conforme o projeto começar a pedi-los.