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:
npm create vite@latest my-react-app -- --template react-tsArquivos 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:
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.
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:
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:
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:
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.
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.
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.
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:
// 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.
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.
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.

