TypeScript

React com TypeScript: Tipando Props, State e Eventos na Prática

16 min de leitura

React com TypeScript: Tipando Props, State e Eventos na Prática

Introdução: Por que Tipificar em React com TypeScript 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. 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. Tipando Props: A Porta de Entrada do Seu Componente Interfaces vs Types para Props Em TypeScript, você pode

<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 &#039;react&#039;;

interface ButtonProps {

label: string;

onClick: () =&gt; void;

disabled?: boolean;

variant?: &#039;primary&#039; | &#039;secondary&#039;;

}

const Button: React.FC&lt;ButtonProps&gt; = ({ label, onClick, disabled = false, variant = &#039;primary&#039; }) =&gt; {

return (

&lt;button

onClick={onClick}

disabled={disabled}

className={btn btn-${variant}}

&gt;

{label}

&lt;/button&gt;

);

};

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 &#039;primary&#039; ou &#039;secondary&#039;. 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) =&gt; void;

actions?: React.ReactNode;

children?: React.ReactNode;

}

const UserCard: React.FC&lt;UserCardProps&gt; = ({ user, onUserClick, actions, children }) =&gt; {

return (

&lt;div className=&quot;user-card&quot;&gt;

&lt;div className=&quot;user-info&quot;&gt;

{user.avatar &amp;&amp; &lt;img src={user.avatar} alt={user.name} /&gt;}

&lt;h3&gt;{user.name}&lt;/h3&gt;

&lt;p&gt;{user.email}&lt;/p&gt;

&lt;/div&gt;

&lt;div className=&quot;user-actions&quot;&gt;

{actions}

{children}

&lt;/div&gt;

&lt;button onClick={() =&gt; onUserClick(user.id)}&gt;View Profile&lt;/button&gt;

&lt;/div&gt;

);

};

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&lt;TProps&gt;</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) =&gt; void;

}

const Input: React.FC&lt;InputProps&gt; = ({

placeholder = &#039;Digite algo...&#039;,

type = &#039;text&#039;,

maxLength = 100,

onChange

}) =&gt; {

return (

&lt;input

type={type}

placeholder={placeholder}

maxLength={maxLength}

onChange={(e) =&gt; onChange(e.target.value)}

/&gt;

);

};

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 &#039;react&#039;;

interface FormState {

username: string;

password: string;

rememberMe: boolean;

}

const LoginForm: React.FC = () =&gt; {

const [formData, setFormData] = useState&lt;FormState&gt;({

username: &#039;&#039;,

password: &#039;&#039;,

rememberMe: false,

});

const [isLoading, setIsLoading] = useState(false);

const [error, setError] = useState&lt;string | null&gt;(null);

const handleChange = (field: keyof FormState, value: string | boolean) =&gt; {

setFormData((prev) =&gt; ({

...prev,

[field]: value,

}));

};

const handleSubmit = async (e: React.FormEvent) =&gt; {

e.preventDefault();

setIsLoading(true);

setError(null);

try {

// Simular chamada à API

await new Promise((resolve) =&gt; setTimeout(resolve, 1000));

console.log(&#039;Login realizado:&#039;, formData);

} catch (err) {

setError(&#039;Falha ao fazer login. Tente novamente.&#039;);

} finally {

setIsLoading(false);

}

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;input

type=&quot;text&quot;

value={formData.username}

onChange={(e) =&gt; handleChange(&#039;username&#039;, e.target.value)}

placeholder=&quot;Usuário&quot;

/&gt;

&lt;input

type=&quot;password&quot;

value={formData.password}

onChange={(e) =&gt; handleChange(&#039;password&#039;, e.target.value)}

placeholder=&quot;Senha&quot;

/&gt;

&lt;label&gt;

&lt;input

type=&quot;checkbox&quot;

checked={formData.rememberMe}

onChange={(e) =&gt; handleChange(&#039;rememberMe&#039;, e.target.checked)}

/&gt;

Manter-me conectado

&lt;/label&gt;

{error &amp;&amp; &lt;div className=&quot;error&quot;&gt;{error}&lt;/div&gt;}

&lt;button type=&quot;submit&quot; disabled={isLoading}&gt;

{isLoading ? &#039;Entrando...&#039; : &#039;Entrar&#039;}

&lt;/button&gt;

&lt;/form&gt;

);

};

export default LoginForm;</code></pre>

<p>Observe como <code>useState&lt;FormState&gt;</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 &quot;discriminated unions&quot; — tipos que têm um campo comum que diferencia suas formas:</p>

<pre><code class="language-typescript">import React, { useState } from &#039;react&#039;;

type AsyncState&lt;T&gt; =

{ status: &#039;idle&#039; } | { status: &#039;loading&#039; } | { status: &#039;success&#039;; data: T } | { status: &#039;error&#039;; error: string };

interface Post {

id: number;

title: string;

content: string;

}

const PostList: React.FC = () =&gt; {

const [state, setState] = useState&lt;AsyncState&lt;Post[]&gt;&gt;({ status: &#039;idle&#039; });

const fetchPosts = async () =&gt; {

setState({ status: &#039;loading&#039; });

try {

// Simular fetch de API

await new Promise((resolve) =&gt; setTimeout(resolve, 1000));

const data: Post[] = [

{ id: 1, title: &#039;Post 1&#039;, content: &#039;Conteúdo 1&#039; },

{ id: 2, title: &#039;Post 2&#039;, content: &#039;Conteúdo 2&#039; },

];

setState({ status: &#039;success&#039;, data });

} catch (err) {

setState({ status: &#039;error&#039;, error: &#039;Falha ao carregar posts&#039; });

}

};

return (

&lt;div&gt;

&lt;button onClick={fetchPosts}&gt;Carregar Posts&lt;/button&gt;

{state.status === &#039;idle&#039; &amp;&amp; &lt;p&gt;Clique para carregar&lt;/p&gt;}

{state.status === &#039;loading&#039; &amp;&amp; &lt;p&gt;Carregando...&lt;/p&gt;}

{state.status === &#039;success&#039; &amp;&amp; (

&lt;ul&gt;

{state.data.map((post) =&gt; (

&lt;li key={post.id}&gt;{post.title}&lt;/li&gt;

))}

&lt;/ul&gt;

)}

{state.status === &#039;error&#039; &amp;&amp; &lt;p className=&quot;error&quot;&gt;{state.error}&lt;/p&gt;}

&lt;/div&gt;

);

};

export default PostList;</code></pre>

<p>Este padrão é extremamente poderoso. TypeScript entende que quando <code>status</code> é &#039;success&#039;, a propriedade <code>data</code> está disponível. Se você tentar acessar <code>data</code> quando o status não é &#039;success&#039;, 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 &#039;react&#039;;

interface SearchBoxProps {

onSearch: (query: string) =&gt; void;

onFocus?: () =&gt; void;

onBlur?: () =&gt; void;

}

const SearchBox: React.FC&lt;SearchBoxProps&gt; = ({ onSearch, onFocus, onBlur }) =&gt; {

const handleChange = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {

onSearch(e.target.value);

};

const handleKeyPress = (e: React.KeyboardEvent&lt;HTMLInputElement&gt;) =&gt; {

if (e.key === &#039;Enter&#039;) {

console.log(&#039;Busca realizada&#039;);

}

};

const handleClick = (e: React.MouseEvent&lt;HTMLButtonElement&gt;) =&gt; {

e.preventDefault();

console.log(&#039;Botão clicado&#039;);

};

return (

&lt;div&gt;

&lt;input

type=&quot;text&quot;

placeholder=&quot;Buscar...&quot;

onChange={handleChange}

onKeyPress={handleKeyPress}

onFocus={onFocus}

onBlur={onBlur}

/&gt;

&lt;button onClick={handleClick}&gt;Buscar&lt;/button&gt;

&lt;/div&gt;

);

};

export default SearchBox;</code></pre>

<p><code>React.ChangeEvent&lt;HTMLInputElement&gt;</code>, <code>React.KeyboardEvent&lt;HTMLInputElement&gt;</code> e <code>React.MouseEvent&lt;HTMLButtonElement&gt;</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 &#039;react&#039;;

interface TodoItem {

id: number;

text: string;

completed: boolean;

}

interface TodoListProps {

todos: TodoItem[];

onToggleTodo: (id: number) =&gt; void;

onDeleteTodo: (id: number) =&gt; void;

onEditTodo: (id: number, newText: string) =&gt; void;

}

const TodoList: React.FC&lt;TodoListProps&gt; = ({

todos,

onToggleTodo,

onDeleteTodo,

onEditTodo,

}) =&gt; {

const [editingId, setEditingId] = React.useState&lt;number | null&gt;(null);

const [editText, setEditText] = React.useState(&#039;&#039;);

const handleDoubleClick = (todo: TodoItem) =&gt; {

setEditingId(todo.id);

setEditText(todo.text);

};

const handleSaveEdit = (id: number) =&gt; {

onEditTodo(id, editText);

setEditingId(null);

};

const handleKeyDown = (e: React.KeyboardEvent&lt;HTMLInputElement&gt;, id: number) =&gt; {

if (e.key === &#039;Enter&#039;) {

handleSaveEdit(id);

} else if (e.key === &#039;Escape&#039;) {

setEditingId(null);

}

};

return (

&lt;ul&gt;

{todos.map((todo) =&gt; (

&lt;li key={todo.id}&gt;

{editingId === todo.id ? (

&lt;input

type=&quot;text&quot;

value={editText}

onChange={(e) =&gt; setEditText(e.target.value)}

onKeyDown={(e) =&gt; handleKeyDown(e, todo.id)}

autoFocus

/&gt;

) : (

&lt;&gt;

&lt;input

type=&quot;checkbox&quot;

checked={todo.completed}

onChange={() =&gt; onToggleTodo(todo.id)}

/&gt;

&lt;span

onDoubleClick={() =&gt; handleDoubleClick(todo)}

style={{

textDecoration: todo.completed ? &#039;line-through&#039; : &#039;none&#039;,

}}

&gt;

{todo.text}

&lt;/span&gt;

&lt;button onClick={() =&gt; onDeleteTodo(todo.id)}&gt;Deletar&lt;/button&gt;

&lt;/&gt;

)}

&lt;/li&gt;

))}

&lt;/ul&gt;

);

};

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 &#039;react&#039;;

interface SubmitButtonProps {

onSubmit: (data: string) =&gt; Promise&lt;void&gt;;

label?: string;

}

const SubmitButton: React.FC&lt;SubmitButtonProps&gt; = ({ onSubmit, label = &#039;Enviar&#039; }) =&gt; {

const [isLoading, setIsLoading] = React.useState(false);

const [error, setError] = React.useState&lt;string | null&gt;(null);

const handleClick = async (e: React.MouseEvent&lt;HTMLButtonElement&gt;) =&gt; {

e.preventDefault();

setIsLoading(true);

setError(null);

try {

await onSubmit(&#039;dados de exemplo&#039;);

} catch (err) {

setError(err instanceof Error ? err.message : &#039;Erro desconhecido&#039;);

} finally {

setIsLoading(false);

}

};

return (

&lt;&gt;

&lt;button onClick={handleClick} disabled={isLoading}&gt;

{isLoading ? &#039;Processando...&#039; : label}

&lt;/button&gt;

{error &amp;&amp; &lt;div className=&quot;error&quot;&gt;{error}&lt;/div&gt;}

&lt;/&gt;

);

};

export default SubmitButton;</code></pre>

<p>Repare que <code>onSubmit</code> tem tipo <code>(data: string) =&gt; Promise&lt;void&gt;</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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Dominando Mixins em TypeScript: Composição de Comportamentos sem Herança em Projetos Reais
Dominando Mixins em TypeScript: Composição de Comportamentos sem Herança em Projetos Reais

O Problema da Herança Clássica Quando começamos a programar orientada a objet...

Guia Completo de Formulários com TypeScript: React Hook Form e Zod Integrados
Guia Completo de Formulários com TypeScript: React Hook Form e Zod Integrados

Introdução: Por que React Hook Form com Zod? Trabalhar com formulários em Rea...

Guia Completo de CLI com TypeScript: Construindo Ferramentas de Linha de Comando Tipadas
Guia Completo de CLI com TypeScript: Construindo Ferramentas de Linha de Comando Tipadas

Introdução: Por que TypeScript em CLIs? Quando você desenvolve aplicações de...