<h2>Introdução: Por que Tipificar em React com TypeScript</h2>
<p>React é uma biblioteca poderosa para construir interfaces de usuário, mas quando você trabalha em projetos médios ou grandes, o JavaScript puro pode deixar falhas escaparem apenas em tempo de execução. TypeScript resolve isso adicionando um sistema de tipos robusto que permite detectar erros antes do código chegar à produção. O desafio começa quando precisamos tipar corretamente props, state e handlers de eventos — elementos fundamentais de qualquer componente React.</p>
<p>A tipificação não é apenas uma camada de segurança; ela é documentação viva no seu código. Quando um desenvolvedor novo entra no projeto e vê uma prop tipificada, ele imediatamente compreende qual tipo de dado deve ser passado e qual comportamento esperar. Nesta aula, vamos explorar as melhores práticas para tipar esses três pilares do React, evitando armadilhas comuns e desenvolvendo componentes verdadeiramente reutilizáveis.</p>
<h2>Tipando Props: A Porta de Entrada do Seu Componente</h2>
<h3>Interfaces vs Types para Props</h3>
<p>Em TypeScript, você pode usar tanto <code>interface</code> quanto <code>type</code> para descrever props. Historicamente, a comunidade React preferia <code>interface</code>, mas ambas funcionam perfeitamente. A escolha geralmente é estética, embora <code>interface</code> seja ligeiramente mais legível quando você está começando. Vou usar <code>interface</code> nos exemplos, mas sinta-se livre para usar <code>type</code> — eles são intercambiáveis para este caso de uso.</p>
<p>Props são simplesmente argumentos que você passa para um componente. Para garantir que o componente receba exatamente o que espera, você define uma interface que descreve a forma desses dados. Veja este exemplo prático:</p>
<pre><code class="language-typescript">import React from 'react';
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary';
}
const Button: React.FC<ButtonProps> = ({ label, onClick, disabled = false, variant = 'primary' }) => {
return (
<button
onClick={onClick}
disabled={disabled}
className={btn btn-${variant}}
>
{label}
</button>
);
};
export default Button;</code></pre>
<p>Aqui, <code>ButtonProps</code> define claramente que o componente espera uma string <code>label</code>, uma função <code>onClick</code>, e opcionalmente um booleano <code>disabled</code> e uma string <code>variant</code> que só pode ser 'primary' ou 'secondary'. O <code>?</code> após o nome da propriedade a torna opcional. Quando você usar este componente em outro lugar, o TypeScript vai alertá-lo se tentar passar um valor inválido.</p>
<h3>Props Complexas e Composição</h3>
<p>Nem sempre suas props são primitivos simples. Frequentemente você precisa passar objetos, arrays ou até mesmo outros componentes React. Para esses casos, TypeScript oferece tipos mais avançados que ajudam a manter a segurança.</p>
<pre><code class="language-typescript">interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
interface UserCardProps {
user: User;
onUserClick: (userId: number) => void;
actions?: React.ReactNode;
children?: React.ReactNode;
}
const UserCard: React.FC<UserCardProps> = ({ user, onUserClick, actions, children }) => {
return (
<div className="user-card">
<div className="user-info">
{user.avatar && <img src={user.avatar} alt={user.name} />}
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
<div className="user-actions">
{actions}
{children}
</div>
<button onClick={() => onUserClick(user.id)}>View Profile</button>
</div>
);
};
export default UserCard;</code></pre>
<p>Note que <code>React.ReactNode</code> é usado para <code>actions</code> e <code>children</code> — isso permite que você passe praticamente qualquer coisa renderizável (strings, números, componentes, arrays). Quando você precisa ser mais restritivo e aceitar apenas um componente específico, use <code>React.ComponentType<TProps></code> ou simplesmente <code>JSX.Element</code>. O tipo <code>User</code> é composável: você pode reutilizá-lo em várias interfaces, evitando duplicação.</p>
<h3>Valores Padrão e Props Obrigatórias</h3>
<p>TypeScript ajuda você a identificar quando uma prop é obrigatória versus opcional, mas muitas vezes você quer fornecer um valor padrão que JavaScript entende naturalmente. Use a desestruturação com padrões no parâmetro da função:</p>
<pre><code class="language-typescript">interface InputProps {
placeholder?: string;
type?: string;
maxLength?: number;
onChange: (value: string) => void;
}
const Input: React.FC<InputProps> = ({
placeholder = 'Digite algo...',
type = 'text',
maxLength = 100,
onChange
}) => {
return (
<input
type={type}
placeholder={placeholder}
maxLength={maxLength}
onChange={(e) => onChange(e.target.value)}
/>
);
};
export default Input;</code></pre>
<p>Aqui, <code>placeholder</code>, <code>type</code> e <code>maxLength</code> têm valores padrão, então o componente funciona perfeitamente mesmo que não sejam passados. Mas <code>onChange</code> é obrigatório — o TypeScript vai reclamar se você tentar usar o componente sem fornecê-lo.</p>
<h2>Tipando State: Mantendo a Segurança Interna</h2>
<h3>useState com Tipos Explícitos</h3>
<p>O estado interno de um componente é onde a lógica dinâmica acontece. Quando você usa <code>useState</code>, o TypeScript tenta inferir o tipo baseado no valor inicial, mas é uma excelente prática ser explícito, especialmente quando o estado pode ser <code>null</code> ou ter múltiplas formas.</p>
<pre><code class="language-typescript">import React, { useState } from 'react';
interface FormState {
username: string;
password: string;
rememberMe: boolean;
}
const LoginForm: React.FC = () => {
const [formData, setFormData] = useState<FormState>({
username: '',
password: '',
rememberMe: false,
});
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleChange = (field: keyof FormState, value: string | boolean) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError(null);
try {
// Simular chamada à API
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('Login realizado:', formData);
} catch (err) {
setError('Falha ao fazer login. Tente novamente.');
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData.username}
onChange={(e) => handleChange('username', e.target.value)}
placeholder="Usuário"
/>
<input
type="password"
value={formData.password}
onChange={(e) => handleChange('password', e.target.value)}
placeholder="Senha"
/>
<label>
<input
type="checkbox"
checked={formData.rememberMe}
onChange={(e) => handleChange('rememberMe', e.target.checked)}
/>
Manter-me conectado
</label>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={isLoading}>
{isLoading ? 'Entrando...' : 'Entrar'}
</button>
</form>
);
};
export default LoginForm;</code></pre>
<p>Observe como <code>useState<FormState></code> deixa claro que o estado tem exatamente essa forma. Para <code>error</code>, usamos <code>string | null</code> porque inicialmente é <code>null</code>, mas pode se tornar uma mensagem de erro. Usar <code>keyof FormState</code> em <code>handleChange</code> garante que você só possa atualizar campos que realmente existem.</p>
<h3>Estados Complexos e Discriminated Unions</h3>
<p>Conforme seus componentes crescem, o estado pode assumir múltiplas formas. Uma técnica poderosa é usar "discriminated unions" — tipos que têm um campo comum que diferencia suas formas:</p>
<pre><code class="language-typescript">import React, { useState } from 'react';
type AsyncState<T> =
{ status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: string };
interface Post {
id: number;
title: string;
content: string;
}
const PostList: React.FC = () => {
const [state, setState] = useState<AsyncState<Post[]>>({ status: 'idle' });
const fetchPosts = async () => {
setState({ status: 'loading' });
try {
// Simular fetch de API
await new Promise((resolve) => setTimeout(resolve, 1000));
const data: Post[] = [
{ id: 1, title: 'Post 1', content: 'Conteúdo 1' },
{ id: 2, title: 'Post 2', content: 'Conteúdo 2' },
];
setState({ status: 'success', data });
} catch (err) {
setState({ status: 'error', error: 'Falha ao carregar posts' });
}
};
return (
<div>
<button onClick={fetchPosts}>Carregar Posts</button>
{state.status === 'idle' && <p>Clique para carregar</p>}
{state.status === 'loading' && <p>Carregando...</p>}
{state.status === 'success' && (
<ul>
{state.data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)}
{state.status === 'error' && <p className="error">{state.error}</p>}
</div>
);
};
export default PostList;</code></pre>
<p>Este padrão é extremamente poderoso. TypeScript entende que quando <code>status</code> é 'success', a propriedade <code>data</code> está disponível. Se você tentar acessar <code>data</code> quando o status não é 'success', ele vai reclamar. Isso elimina uma classe inteira de bugs de runtime.</p>
<h2>Tipando Eventos: Handlers Seguros e Previsíveis</h2>
<h3>Tipos de Eventos do React</h3>
<p>React envolve eventos nativos do DOM em seus próprios tipos. Quando você trabalha com eventos em TypeScript, precisa usar os tipos corretos fornecidos pelo React. Cada tipo de elemento e evento tem um tipo correspondente.</p>
<pre><code class="language-typescript">import React from 'react';
interface SearchBoxProps {
onSearch: (query: string) => void;
onFocus?: () => void;
onBlur?: () => void;
}
const SearchBox: React.FC<SearchBoxProps> = ({ onSearch, onFocus, onBlur }) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onSearch(e.target.value);
};
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
console.log('Busca realizada');
}
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
console.log('Botão clicado');
};
return (
<div>
<input
type="text"
placeholder="Buscar..."
onChange={handleChange}
onKeyPress={handleKeyPress}
onFocus={onFocus}
onBlur={onBlur}
/>
<button onClick={handleClick}>Buscar</button>
</div>
);
};
export default SearchBox;</code></pre>
<p><code>React.ChangeEvent<HTMLInputElement></code>, <code>React.KeyboardEvent<HTMLInputElement></code> e <code>React.MouseEvent<HTMLButtonElement></code> são os tipos corretos. Note que você especifica qual elemento HTML o evento vem — isso permite que o TypeScript saiba quais propriedades estão disponíveis em <code>e.target</code>.</p>
<h3>Handlers Parametrizados e Callbacks</h3>
<p>Muitas vezes você quer passar dados do evento para a função callback. Aqui está uma abordagem limpa:</p>
<pre><code class="language-typescript">import React from 'react';
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
interface TodoListProps {
todos: TodoItem[];
onToggleTodo: (id: number) => void;
onDeleteTodo: (id: number) => void;
onEditTodo: (id: number, newText: string) => void;
}
const TodoList: React.FC<TodoListProps> = ({
todos,
onToggleTodo,
onDeleteTodo,
onEditTodo,
}) => {
const [editingId, setEditingId] = React.useState<number | null>(null);
const [editText, setEditText] = React.useState('');
const handleDoubleClick = (todo: TodoItem) => {
setEditingId(todo.id);
setEditText(todo.text);
};
const handleSaveEdit = (id: number) => {
onEditTodo(id, editText);
setEditingId(null);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, id: number) => {
if (e.key === 'Enter') {
handleSaveEdit(id);
} else if (e.key === 'Escape') {
setEditingId(null);
}
};
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{editingId === todo.id ? (
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyDown={(e) => handleKeyDown(e, todo.id)}
autoFocus
/>
) : (
<>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggleTodo(todo.id)}
/>
<span
onDoubleClick={() => handleDoubleClick(todo)}
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
}}
>
{todo.text}
</span>
<button onClick={() => onDeleteTodo(todo.id)}>Deletar</button>
</>
)}
</li>
))}
</ul>
);
};
export default TodoList;</code></pre>
<p>Aqui, <code>onKeyDown</code> recebe o evento e o <code>id</code> como parâmetros separados. TypeScript verifica que ambos têm os tipos corretos. O handler <code>handleDoubleClick</code> extrai dados do objeto <code>todo</code> antes de passar para a função callback — isso torna o código mais legível e type-safe.</p>
<h3>Async Handlers e Promises</h3>
<p>Quando seus handlers são assíncronos, você precisa comunicar isso claramente através dos tipos:</p>
<pre><code class="language-typescript">import React from 'react';
interface SubmitButtonProps {
onSubmit: (data: string) => Promise<void>;
label?: string;
}
const SubmitButton: React.FC<SubmitButtonProps> = ({ onSubmit, label = 'Enviar' }) => {
const [isLoading, setIsLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const handleClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setIsLoading(true);
setError(null);
try {
await onSubmit('dados de exemplo');
} catch (err) {
setError(err instanceof Error ? err.message : 'Erro desconhecido');
} finally {
setIsLoading(false);
}
};
return (
<>
<button onClick={handleClick} disabled={isLoading}>
{isLoading ? 'Processando...' : label}
</button>
{error && <div className="error">{error}</div>}
</>
);
};
export default SubmitButton;</code></pre>
<p>Repare que <code>onSubmit</code> tem tipo <code>(data: string) => Promise<void></code>. Isso documenta que a função é assíncrona e retorna uma Promise vazia. Quando você chama <code>await onSubmit(...)</code>, TypeScript entende automaticamente que precisa esperar a conclusão. O tratamento de erro diferencia entre instâncias de <code>Error</code> e outros valores lançados.</p>
<h2>Conclusão</h2>
<p>Tipar Props, State e Eventos em React com TypeScript não é apenas uma formalidade — é um investimento direto na qualidade e manutenibilidade do seu código. Ao usar <code>interface</code> para descrever props, tipos genéricos para estado e tipos de evento específicos do React, você cria um escudo contra erros que só seriam descobertos em produção com JavaScript puro. A segunda lição é que <strong>discriminated unions</strong> (tipos que usam um campo comum para distinguir variantes) são incrivelmente poderosos para estados complexos, eliminando a necessidade de booleanos redundantes e lógica condicional confusa. Por fim, <strong>ser explícito é melhor que implícito</strong> — especificar tipos mesmo quando TypeScript poderia inferir é um padrão profissional que facilita onboarding, refatoração e depuração em projetos reais.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.typescriptlang.org/docs/handbook/react.html" target="_blank" rel="noopener noreferrer">TypeScript Official Documentation - React</a></li>
<li><a href="https://react-typescript-cheatsheet.netlify.app/" target="_blank" rel="noopener noreferrer">React TypeScript Cheatsheet</a></li>
<li><a href="https://www.w3schools.com/react/react_typescript.asp" target="_blank" rel="noopener noreferrer">React Event Handling with TypeScript</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/advanced-types.html" target="_blank" rel="noopener noreferrer">Advanced Types in TypeScript</a></li>
<li><a href="https://kentcdodds.com/blog/how-to-write-a-react-component-in-typescript" target="_blank" rel="noopener noreferrer">Kent C. Dodds - TypeScript and React</a></li>
</ul>
<p><!-- FIM --></p>