Menu

React com TypeScript: tipando props, estado e eventos

Como tipar um app React com TypeScript: props e children, useState com unions e null, useRef para elementos do DOM, event handlers, context, componentes genéricos e props de elementos nativos.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

React com TypeScript significa escrever componentes em arquivos .tsx em que props, estado, refs e event handlers têm tipos, para que o compilador pegue uma prop faltando ou um event handler errado antes de o código executar. Os tipos desaparecem no build: o que roda no navegador é o mesmo JavaScript que você teria escrito sem eles. Os editores ao vivo desta página executam esse JavaScript, e a versão tipada fica ao lado de cada um em um bloco estático.

Aqui está o mesmo Badge com tipos. count é opcional (?), então o segundo badge cai no padrão 0, e children aceita tanto um elemento quanto uma string simples.

type BadgeProps = {
    label: string;
    count?: number;
    children: React.ReactNode;
};

function Badge({ label, count = 0, children }: BadgeProps) {
    return (
        <div>
            <strong>{label}</strong> ({count})
            <div>{children}</div>
        </div>
    );
}

<Badge count={3}>Hi</Badge>;
// Error: Property 'label' is missing in type '{ children: string; count: number; }'

Criando um projeto com TypeScript

O Vite tem um template de React com TypeScript:

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

Você recebe arquivos .tsx, um tsconfig.json com strict ligado e as definições de tipos do React (@types/react, @types/react-dom) já instaladas. O Vite remove os tipos enquanto serve e faz o build, mas não os verifica; o script build do template roda tsc antes, e o seu editor verifica enquanto você digita. Frameworks como o Next.js configuram o TypeScript do mesmo jeito quando você cria um projeto com a CLI deles.

Tipando props

Descreva as props como um tipo de objeto e anote o parâmetro desestruturado. Alguns padrões cobrem a maioria dos componentes:

interface UserCardProps {
    name: string;                       // required
    age?: number;                       // optional, may be undefined
    role: 'admin' | 'member';           // a union of allowed strings
    tags: string[];
    onSelect: (id: string) => void;     // a callback prop
    children?: React.ReactNode;         // anything React can render
}

function UserCard({ name, age, role, tags, onSelect, children }: UserCardProps) {
    // ...
}

type ou interface. Os dois funcionam para props. interface pode ser estendida com extends e mesclada ao ser declarada duas vezes; type também consegue expressar unions, intersections e mapped types. A maioria das equipes escolhe um e usa em todo lugar.

children. Use React.ReactNode para tudo o que vai entre as tags: elementos, strings, números, arrays, null. A página sobre props explica como children chega ao componente.

Por que React.FC é opcional. const Badge: React.FC<BadgeProps> = (...) => ... tipa a função inteira em vez do parâmetro. Desde os tipos do React 18 ele não adiciona mais children para você, não aceita um parâmetro de tipo para um componente genérico e não acrescenta nada que um parâmetro tipado não faça. Código mais antigo o usa bastante; funções simples com props tipadas são a escolha comum em código novo.

Tipando o useState

Para um valor inicial simples, a inferência basta: useState(0) é um number, useState('') uma string. Passe um argumento de tipo quando o estado pode guardar mais do que o valor inicial sugere: um valor que começa como null ou uma entre várias strings fixas.

Clique em "Load user" e o status passa de idle para loading e para done. O estado tipado:

type User = { id: number; name: string };
type Status = 'idle' | 'loading' | 'done';

const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('idle');

user.name;          // Error: 'user' is possibly 'null'
user?.name;         // OK
setStatus('ready'); // Error: Argument of type '"ready"' is not assignable to parameter of type 'SetStateAction<Status>'

Sem <User | null>, useState(null) infere o tipo null e setUser(data) não compila. A union obriga você a tratar o caso vazio, que é exatamente a verificação user ? ... : ... do exemplo em execução.

useRef para elementos do DOM

Uma ref que aponta para um elemento do DOM recebe o tipo do elemento e começa como null, porque o elemento não existe até o React conectá-lo depois da primeira renderização.

Digite algo e depois clique em "Log the value": o console imprime o que você digitou, lido direto do elemento do DOM. A versão tipada:

const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current is HTMLInputElement | null

inputRef.current?.focus();    // OK
inputRef.current.focus();     // Error: 'inputRef.current' is possibly 'null'

const timerId = useRef<number | null>(null);  // a mutable value, not a DOM node

Use a interface de elemento que corresponde à tag: HTMLInputElement, HTMLDivElement, HTMLCanvasElement, HTMLButtonElement. Nos tipos do React 19, o useRef sempre recebe um argumento e retorna um RefObject cujo current você pode atribuir, então a mesma forma funciona para refs do DOM e para valores como o id de um timer. Veja useRef para o lado da execução.

Tipos de event handlers

Handlers inline não precisam de anotação: em onChange={(e) => setName(e.target.value)} o TypeScript conhece e pela prop para a qual ele é passado. Anote quando você move um handler para uma função própria.

Envie e o console registra o endereço; o preventDefault impede a página de recarregar. Com tipos:

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    setEmail(e.target.value); // e.target is HTMLInputElement, value is string
}

function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
    e.preventDefault();
}

function handleClick(e: React.MouseEvent<HTMLButtonElement>) {}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
    if (e.key === 'Enter') { /* ... */ }
}

Desde o @types/react 19.2.10, o onSubmit é tipado com React.SubmitEvent e o antigo React.FormEvent está marcado como descontinuado; com uma versão mais antiga dos tipos, escreva React.FormEvent<HTMLFormElement>. O parâmetro de tipo é o elemento ao qual o handler está ligado. Se não tiver certeza do tipo, passe o mouse sobre a prop onChange no seu editor ou tipe o handler inteiro de uma vez: const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { ... }.

Tipando context

Dê ao createContext o tipo do valor. Se existir um padrão razoável, passe-o, e todo consumidor recebe um valor não nulo:

type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');

const theme = useContext(ThemeContext); // Theme

Quando não há um bom padrão (um usuário logado, uma store), comece com null e envolva o useContext em um hook que lança um erro se um componente for usado fora do provider. Todo chamador recebe então o tipo não nulo e uma mensagem de erro clara em vez de um travamento lá no fundo.

type Auth = { user: string; logout: () => void };
const AuthContext = createContext<Auth | null>(null);

export function useAuth(): Auth {
    const value = useContext(AuthContext);
    if (!value) throw new Error('useAuth must be used inside <AuthProvider>');
    return value;
}

export function AuthProvider({ children }: { children: React.ReactNode }) {
    const [user, setUser] = useState('Ada');
    return (
        <AuthContext value={{ user, logout: () => setUser('') }}>
            {children}
        </AuthContext>
    );
}

<AuthContext value={...}> é a sintaxe de provider do React 19; <AuthContext.Provider value={...}> é tipado do mesmo jeito.

Componentes genéricos

Uma lista, tabela ou select que funciona com qualquer tipo de item é um componente genérico: um parâmetro de tipo liga a prop items à callback renderItem.

O List tipado:

type ListProps<T> = {
    items: T[];
    renderItem: (item: T) => React.ReactNode;
    getKey: (item: T) => string | number;
};

function List<T>({ items, renderItem, getKey }: ListProps<T>) {
    return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}

<List items={books} getKey={(b) => b.isbn} renderItem={(b) => b.title} />;
// b is inferred as the book type, so b.titel would be an error

O TypeScript infere T a partir de items, então quem chama nunca escreve o parâmetro de tipo. Em um arquivo .tsx, uma arrow function precisa de <T,> em vez de <T>, porque <T> sozinho é lido como uma tag JSX.

Estendendo as props de elementos nativos

Um wrapper em volta de um button ou input deve aceitar todos os atributos que o elemento nativo aceita. React.ComponentProps<'button'> é esse conjunto completo; adicione as suas próprias props por cima e espalhe o resto.

type ButtonProps = React.ComponentProps<'button'> & {
    variant?: 'primary' | 'ghost';
};

function Button({ variant = 'primary', style, ...rest }: ButtonProps) {
    return (
        <button
            {...rest}
            style={{ fontWeight: variant === 'primary' ? 600 : 400, ...style }}
        />
    );
}

<Button type="submit" disabled onClick={(e) => console.log(e.currentTarget)}>
    Save
</Button>;

onClick, disabled, type e aria-* ficam todos tipados sem precisar listá-los. No React 19, ref é uma prop comum, e ComponentProps<'button'> a inclui, então <Button ref={buttonRef}> chega ao nó do DOM pelo spread, sem forwardRef. Use ComponentPropsWithoutRef<'button'> quando quiser deixar ref de fora, e ComponentProps<typeof UserCard> para ler as props de um dos seus próprios componentes.

Erros de tipo comuns

"'ref.current' is possibly 'null'". O elemento não existe durante a primeira renderização. Use ref.current?.focus() ou confira if (ref.current) antes de usá-lo.

"Type 'string' is not assignable" em um estado com union. const [status, setStatus] = useState('idle') infere string, que é mais amplo do que você quer, e passá-lo para uma prop tipada como Status falha. Escreva useState<Status>('idle').

Evento tipado com o elemento errado. React.ChangeEvent<HTMLInputElement> em um select deixa e.target com o tipo errado. O parâmetro de tipo precisa corresponder ao elemento ao qual o handler está ligado.

Usar any para silenciar um erro. Ele desliga a verificação para tudo em que esse valor toca. Prefira unknown e restrinja o tipo, ou corrija o tipo na origem.

Perguntas frequentes

Como crio um app React com TypeScript?

Rode npm create vite@latest my-app -- --template react-ts, depois npm install e npm run dev. Os componentes ficam em arquivos .tsx e o Vite remove os tipos no build; rode tsc (o script build do template faz isso) para verificá-los.

Qual é o tipo de children no React?

React.ReactNode. Ele aceita tudo o que o React consegue renderizar: elementos, strings, números, arrays, null e undefined. Use React.ReactElement só quando você precisar de exatamente um elemento.

Como tipo o useState com null?

Passe o tipo explicitamente como uma union: useState<User | null>(null). Sem o argumento de tipo, o TypeScript infere null como único valor permitido.

Qual é o tipo de um evento onChange no React?

React.ChangeEvent<HTMLInputElement> para um input (HTMLTextAreaElement ou HTMLSelectElement para esses elementos). Assim e.target.value fica tipado como string.

Devo usar React.FC?

É opcional. Tipar diretamente o parâmetro das props (function Card({ title }: CardProps)) faz o mesmo trabalho, é mais simples de ler e funciona com genéricos. O React.FC não adiciona mais children implicitamente, então oferece pouco a mais.

Devo usar type ou interface para as props?

Os dois funcionam. interface pode ser estendida e mesclada; type pode expressar unions e mapped types. Escolha uma convenção para o código e mantenha.

Ilustração das linguagens de programação do Coddy

Aprenda a programar com o Coddy

COMEÇAR