TypeScript com React: tipando props, hooks e eventos
TypeScript trouxe um novo patamar de segurança e produtividade para o desenvolvimento React. Ao adicionar tipagem estática a props, hooks e eventos, eliminamos uma classe inteira de bugs em tempo de execução e melhoramos drasticamente a experiência de desenvolvimento com autocompletar e refatoração segura. Neste artigo, exploraremos na prática como tipar corretamente cada aspecto de uma aplicação React com TypeScript.
1. Configuração Inicial e Fundamentos
Para começar, o Vite é atualmente a ferramenta recomendada para criar projetos React com TypeScript:
npm create vite@latest meu-projeto -- --template react-ts
O Vite já inclui @types/react e @types/react-dom como dependências. Quanto à escolha entre interface e type para componentes, a recomendação moderna é usar type para props, pois oferece maior flexibilidade com uniões e interseções:
// Usando type (recomendado)
type ButtonProps = {
label: string;
variant?: 'primary' | 'secondary';
};
// Usando interface (alternativa válida)
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary';
}
2. Tipando Props de Componentes
Props opcionais são marcadas com ?, e children deve ser tipado como React.ReactNode:
type CardProps = {
title: string;
description?: string;
children: React.ReactNode;
};
function Card({ title, description, children }: CardProps) {
return (
<div>
<h2>{title}</h2>
{description && <p>{description}</p>}
{children}
</div>
);
}
Para componentes reutilizáveis, use genéricos:
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
};
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}
// Uso
<List items={[1, 2, 3]} renderItem={(item) => <li>{item}</li>} />
3. Tipando Estados com useState
O TypeScript infere automaticamente o tipo baseado no valor inicial, mas em casos complexos devemos declarar explicitamente:
// Inferência automática
const [count, setCount] = useState(0); // tipo: number
// Declaração explícita para tipos complexos
type User = {
id: number;
name: string;
email: string;
};
const [user, setUser] = useState<User | null>(null);
// Estados com arrays tipados
const [tags, setTags] = useState<string[]>([]);
// Narrowing para tipos null/undefined
if (user) {
console.log(user.name); // TypeScript sabe que user não é null aqui
}
4. Tipando useEffect e useRef
O useEffect não requer tipagem especial, mas as dependências devem ser consistentes:
useEffect(() => {
const fetchData = async () => {
const response = await fetch('/api/data');
const data: User[] = await response.json();
// processar dados
};
fetchData();
}, []); // dependências vazias = executa uma vez
Para referências a elementos DOM, use os tipos HTML específicos:
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
// useRef para valores mutáveis (sem re-render)
const renderCount = useRef<number>(0);
renderCount.current += 1;
5. Tipando Eventos do DOM
Eventos de formulário e clique são comuns e devem ser tipados corretamente:
// Evento de formulário
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
// lógica de submit
}
// Evento de clique
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
console.log(e.clientX, e.clientY);
}
// Evento de teclado
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') {
// submeter formulário
}
}
// Exemplo completo
function SearchForm() {
const [search, setSearch] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('Buscando:', search);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" value={search} onChange={handleChange} />
<button type="submit">Buscar</button>
</form>
);
}
6. Tipando Custom Hooks
Custom hooks devem ter retorno tipado para garantir segurança:
// Hook com retorno tipado como tupla
function useToggle(initialValue = false): [boolean, () => void] {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(prev => !prev), []);
return [value, toggle];
}
// Hook genérico: useLocalStorage<T>
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T) => {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
};
return [storedValue, setValue];
}
// Uso
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
7. Tipando Context API e Reducers
O Context API com TypeScript exige tipos explícitos:
type Theme = 'light' | 'dark';
type ThemeContextType = {
theme: Theme;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Hook customizado para consumo seguro
function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme deve ser usado dentro de ThemeProvider');
}
return context;
}
Para useReducer com discriminated unions:
type Action =
| { type: 'INCREMENT'; payload: number }
| { type: 'DECREMENT' }
| { type: 'RESET' };
type State = { count: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + action.payload };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
8. Boas Práticas e Padrões Avançados
Evite any a todo custo — prefira unknown quando o tipo for realmente desconhecido:
// Ruim
function processData(data: any) { }
// Bom
function processData(data: unknown) {
if (typeof data === 'string') {
// TypeScript sabe que é string aqui
}
}
Utilitários do React que facilitam a tipagem:
// React.FC é opcional, muitos preferem tipar props diretamente
type MyComponentProps = {
title: string;
};
// React.ComponentProps para extrair props de componentes existentes
type ButtonProps = React.ComponentProps<'button'>;
// React.PropsWithChildren para adicionar children rapidamente
type CardProps = React.PropsWithChildren<{
title: string;
}>;
// Memoização com tipagem correta
const MemoizedComponent = React.memo(function MyComponent({ value }: { value: string }) {
return <div>{value}</div>;
});
Conclusão
Tipar corretamente props, hooks e eventos em React com TypeScript não é apenas uma questão de boas práticas — é uma mudança fundamental na forma como escrevemos código. A segurança de tipos elimina bugs antes mesmo de executarmos o código, melhora a documentação através dos próprios tipos e torna a refatoração muito mais segura. Comece aplicando esses padrões em seus componentes e, gradualmente, você perceberá que o TypeScript se torna um aliado indispensável no desenvolvimento React.
Referências
- Documentação oficial do React com TypeScript — Guia oficial do React sobre como usar TypeScript, incluindo tipagem de props, hooks e eventos
- TypeScript Handbook: React — Documentação oficial do TypeScript sobre integração com React e webpack
- React TypeScript Cheatsheet — Referência completa e atualizada com exemplos práticos de tipagem em React
- Vite: Getting Started with React and TypeScript — Guia oficial do Vite para criar projetos React com TypeScript
- UseReducer com TypeScript: discriminated unions — Artigo de Kent C. Dodds sobre Context API e useReducer com TypeScript