Dia 10 — Componente com TypeScript

Informatica · Conteudo · publicado em 05/10/2026
Dia 10 de 16

Componente com TypeScript

Aula 1

Interface, type e tipar props

Interface, type e tipar props

interface descreve a forma de um objeto: quais campos existem, de que tipo

cada um é, e quais são obrigatórios. Um ponto no fim do nome torna o campo

opcional. A diferença entre interface e type é pequena e o critério é

clareza: interface é para objeto com nome próprio e pode ser prolongada por

outra interface; type é para união, tupla ou intersecção, e aceita as duas

coisas.

type Props = {
  titulo: string;
  subtitulo?: string;
  aoTocar: () => void;
};

function Cartao({ titulo, subtitulo, aoTocar }: Props) {
  return <Text onPress={aoTocar}>{titulo}</Text>;
}

O exemplo desta página roda em JavaScript puro, com as anotações de tipo nos

comentários — o portão executa com Node, onde anotação de tipo não existe. A

forma que a anotação descreve aparece no código que roda: um objeto com titulo,

subtitulo e aoTocar, lido campo por campo. É o mesmo objeto que o type

descreve, e a leitura é a mesma nos dois casos.

Tipar props é o que transforma o erro silencioso em erro de compilação. Sem

tipo, <Cartao titulo={42} /> compila, roda e desenha o número 42 no lugar do

título — o defeito aparece no aparelho, na mão de quem vai usar o componente.

Com tipo, o editor marca o erro antes de o código rodar. Esse é o argumento

prático, e vale mais que qualquer discussão sobre "JavaScript tipado".

O console do exemplo passa pelos três campos e pelos três tipos: titulo sai

como Pedido 41 e responde string no typeof, subtitulo sai como "dois

itens", e aoTocar responde function e devolve "tocou" quando chamada. Os

três campos existem no objeto, os três têm valor, e a anotação `aoTocar:

() => void` é o que impede o autor de ler um retorno que a função não promete.

a diferença entre obrigatório e opcional

Campo obrigatório é nome: string. Campo opcional é nome?: string, e o tipo

passa a ser string | undefined — por isso ler props.nome.length em campo

opcional dá erro de tipo, e está certo dar: o valor pode não estar lá. O

consumo correto é valor padrão na desestruturação, { titulo = 'sem título' },

ou um condicional que trate o ausente.

Os dois console do exemplo mostram o campo obrigatório e o opcional lado a

lado, e o segundo caso é o instructive: um objeto sem subtitulo não quebra.

Com a checagem de undefined o exemplo imprime "sem subtitulo", e com valor

padrão na desestruturação imprime o mesmo texto — duas formas do mesmo

tratamento. A função desestrutura faz a segunda, e os dois objetos dela

produzem "Pedido 41 / dois itens" e "Pedido 42".

O ? não é "campo que pode faltar em alguns casos". Ele é uma afirmação sobre

o tipo: o campo pode ser undefined, e quem escreve o código é obrigado a lidar

com isso. É por isso que o console do exemplo trata o ausente explicitamente em

vez de confiar que o valor existe.

Tipo de função é anotado nos parênteses: (id: string) => void. O void

significa "não devolve nada", e é diferente de devolver null. Função que

altera estado costuma ser () => void, e função que formata valor devolve

string.

A anotação de retorno é o que separa as duas: () => void proíbe usar o

retorno, e () => string obriga a devolver. O exemplo deixa isso visível no

aoTocar, que devolve a string "tocou" apesar de anotado como () => void no

comentário — e é esse o detalhe a conhecer: a anotação descreve o que a função

deve fazer, e o valor que ela realmente devolve é o que o JavaScript vê.

o que o tipo não faz

Tipo é removido em tempo de compilação: ele não existe no aparelho, não ocupa

memória e não protege contra dado errado vindo de fora. Uma resposta de API é

any na prática, e a interface do pedido não transforma o dado em

Pedido — só descreve o que se espera. A checagem real do dado continua sendo

validação em tempo de execução, e é o que garante que a tela não quebre quando

o servidor mandar um campo a menos.

A última linha da saída do exemplo repete isso sem rodear: o tipo é removido na

compilação, não existe no aparelho e não protege dado de fora. É a frase que

resume a metade da aula que não é propaganda de tipagem.

O efeito mais concreto do campo opcional aparece na árvore: com subtitulo

presente a árvore tem dois filhos, e sem ele o exemplo imprime que a árvore ficou

com um. O campo opcional do tipo decide quantos filhos o componente renderiza — e

o null condicional é a forma de o React não contar o filho que não existe.

O lugar certo da anotação é a fronteira: onde o dado entra da rede, o tipo

declara o que a tela espera receber. O resto do caminho herda, e é a interface

do item que serve de contrato para a lista inteira.

A união é o caso em que a interface não serve, e o exemplo a escreve como

array: os estados possíveis são carregando, pronto e erro, e o estado

atual é pronto, que o includes confirma estar na lista. Com anotação de

união esse includes é o que o compilador faz sozinho — o valor que não está

na lista deixa de compilar.

Exemplo

// `interface` e `type` descrevem a forma de um objeto; o `?` torna o
// campo opcional. O exemplo e JavaScript puro, com as anotacoes de tipo
// no texto dos comentarios, porque o portao executa com Node, onde a
// anotacao de tipo nao existe.
const { View, Text, StyleSheet } = require('react-native');

// interface Cartao {
//   titulo: string;          // obrigatorio
//   subtitulo?: string;      // o `?` torna o campo opcional
//   aoTocar: () => void;     // funcao que nao devolve nada
// }
// type Props = Cartao;       // `type` tambem aceita um nome de objeto
// function Cartao({ titulo, subtitulo, aoTocar }: Props) { ... }

// A mesma forma, em objeto de exemplo.
const cartao = {
  titulo: 'Pedido 41',
  subtitulo: 'dois itens',
  aoTocar: () => 'tocou',
};

console.log('campo obrigatorio:', cartao.titulo, '| tipo:', typeof cartao.titulo);
console.log('campo opcional:', cartao.subtitulo);
console.log('funcao anotada:', typeof cartao.aoTocar, '->', cartao.aoTocar());

// Campo opcional ausente: o tipo passa a ser `string | undefined`, e ler
// `.length` nele da erro. O exemplo mostra o tratamento correto.
const semSubtitulo = { titulo: 'Pedido 42', aoTocar: () => 'tocou' };
const valorPadrao = semSubtitulo.subtitulo === undefined ? 'sem subtitulo' : semSubtitulo.subtitulo;
console.log('subtitulo ausente, com valor padrao:', valorPadrao);

// Valor padrao na desestruturacao faz o mesmo trabalho.
function desestrutura(props) {
  const { titulo = 'sem titulo', subtitulo = '' } = props;
  return titulo + (subtitulo ? ' / ' + subtitulo : '');
}
console.log(desestrutura(cartao));
console.log(desestrutura(semSubtitulo));

// `type` para uniao: e o que a `interface` nao faz tao bem.
// type EstadoDeTela = 'carregando' | 'pronto' | 'erro';
const EstadoDeTela = ['carregando', 'pronto', 'erro'];
const estadoAtual = EstadoDeTela[1];
console.log('estados possiveis:', EstadoDeTela);
console.log('o estado atual:', estadoAtual, '| esta na lista?', EstadoDeTela.includes(estadoAtual));

// A arvore montada com props tipadas: o componente so usa os campos que
// a interface declarou.
const estilos = StyleSheet.create({ cartao: { padding: 12 } });
function CartaoTipado(props) {
  return (
    <View style={estilos.cartao}>
      <Text>{props.titulo}</Text>
      {props.subtitulo ? <Text>{props.subtitulo}</Text> : null}
    </View>
  );
}

const arvore = CartaoTipado(cartao);
console.log('tipo:', arvore.type);
console.log('tem dois filhos porque o subtitulo existe:', arvore.props.children.length);
const semSub = CartaoTipado(semSubtitulo);
console.log('sem subtitulo, so um filho:', semSub.props.children.length);
console.log('o campo opcional decide quantos filhos a arvore tem');
console.log('o tipo e removido na compilacao: nao existe no aparelho e nao protege dado de fora');

Saída real

campo obrigatorio: Pedido 41 | tipo: string
campo opcional: dois itens
funcao anotada: function -> tocou
subtitulo ausente, com valor padrao: sem subtitulo
Pedido 41 / dois itens
Pedido 42
estados possiveis: [ 'carregando', 'pronto', 'erro' ]
o estado atual: pronto | esta na lista? true
tipo: View
tem dois filhos porque o subtitulo existe: 2
sem subtitulo, so um filho: 2
o campo opcional decide quantos filhos a arvore tem
o tipo e removido na compilacao: nao existe no aparelho e nao protege dado de fora
Aula 2

Tipar estado, lista e função

Tipar estado, lista e função

Estado tipado é a anotação que vai no useState, e ela cumpre duas funções

diferentes: descrever o que o estado guarda e, em estado com vários campos,

deixar o editor completar o nome do campo. useState<string>('') guarda texto;

useState<number>(0) guarda número; useState<Item[]>([]) guarda lista vazia

de um tipo declarado.

interface Item {
  id: string;
  nome: string;
  total: number;
}

const [itens, setItens] = useState<Item[]>([]);

O exemplo desta página roda em JavaScript puro, com as anotações nos comentários.

A função tipaEstado devolve o valor inicial junto com o typeof dele, e os

três console mostram os três casos: texto como string, número como number

e lista vazia como object. O terceiro é o que interessa — um array vazio não

diz nada sobre o que vai entrar nele, e por isso a linha seguinte do exemplo

avisa que uma lista vazia sem o tipo do elemento não ajuda o editor.

O detalhe que economiza trabalho é a anotação da interface de item junto da

lista. Com ela, itens.map((item) => item.nome) já sabe o que item é, e o

erro de grafada em item.nome aparece antes de rodar. Sem ela, map devolve

any e o editor não ajuda em nada — a tipagem da lista só vale se o tipo do

elemento também existe.

O exemplo aplica isso na prática: nomesDaLista mapeia os três itens e devolve

[ 'Bolo de cenoura', 'Torta de limao', 'Pao de queijo' ], e o reduce soma os

totais em 255. A interface Item com id, nome e total é o contrato

inteiro: qualquer campo lido em qualquer item da lista já está descrito em um

lugar só.

tipar a função e o retorno

Anotação de parâmetro é (item: Item, indice: number). Anotação de retorno é

: string depois do parêntese, e ela força o autor da função a lembrar o

retorno: omitir o return com a anotação presente é erro de compilação, não

undefined silencioso em produção. Esse é o ganho real de tipar retorno — o

erro de caminho aparece antes.

A função resumoDoPedido do exemplo devolve nome e total formatados, e o

map dela devolve os três resumos em ordem. Com a anotação : string, o

retorno é uma string garantida — e é isso que permite concatenar o resultado sem

conferir nada, porque o compilador já disse que é texto.

União é type Resultado = 'ok' | 'erro', e é a forma de impedir o estado

inválido: um estado de tela com cinco valores possíveis é uma cadeia de if,

enquanto com a união o editor recusa o valor que não existe.

O exemplo escreve a união de estado de tela como array de três valores, e a

função classifica devolve sempre um deles: carregando quando está carregando,

erro quando há erro, pronto nos outros casos. Três entradas, três saídas, e

nenhuma delas é um valor fora da lista. É a mesma restrição que a anotação de

união impõe, feita com includes.

O caso mais útil da união é o resultado da busca, e o exemplo o monta com ok e

valor num lado, ok e erro no outro: com dados sai ok: true com três itens,

e sem dados sai ok: false com a mensagem "nenhum registro". Ler valor sem

conferir ok é o erro que a união recusa — em JavaScript puro o valor é

undefined e o erro aparece na tela, muito depois.

Genérico é <T> na função: function primeiro<T>(lista: T[]): T | undefined.

Ele resolve o caso em que a mesma função cuida de lista de pedido e lista de

cliente sem repetir o corpo duas vezes. É o mesmo map tipado do JavaScript,

com a anotação do que entra e do que sai.

O exemplo usa a mesma função em duas listas de tipos diferentes: primeiro de

uma lista de nomes devolve ana, e primeiro de uma lista de itens mapeada

para nome devolve "Bolo de cenoura". O corpo não mudou, e o totalDe de uma

lista vazia devolve 0 — que é o retorno que T | undefined permite sem

estouro.

onde a tipagem economiza mais

O lugar de maior retorno é a fronteira da rede, e não o meio do código. Se o

dado da resposta tem tipo, o erro aparece em um lugar só e o resto do caminho

já está coberto. O meio do código erra sozinho, com o editor apontando. Por isso

o costume que funciona é tipar a função de serviço e o estado que recebe a

resposta, e deixar o resto da tela com o tipo vindo por props.

A árvore do exemplo fecha o argumento: a lista tipada monta três filhos, e cada

filho recebeu o item inteiro — o segundo deles renderiza "Torta de limao", com

nome e total disponíveis. A interface atravessa a lista inteira a partir de

um lugar só, e o componente do item não repete nenhuma anotação porque herda o

tipo pela prop.

O critério para saber se a tipagem está no lugar certo é simples: se o erro

aparece em um arquivo só, na fronteira, a anotação está servindo. Se o mesmo

erro de dado aparece em quatro telas diferentes, o que falta é o tipo da resposta

— não mais anotação espalhada pelo meio do código.

Exemplo

// Estado tipado, interface de item e anotacao de retorno. O exemplo e
// JavaScript puro: as anotacoes ficam no comentario e a forma que elas
// descrevem aparece no codigo que roda.
const { View, Text, StyleSheet } = require('react-native');

// interface Item { id: string; nome: string; total: number; }
// type EstadoTela = 'carregando' | 'pronto' | 'erro';
// type Resultado<T> = { ok: true; valor: T } | { ok: false; erro: string };

// (1) estado tipado: cada hook tem o que guarda.
function tipaEstado(inicial) {
  return { valor: inicial, tipo: typeof inicial };
}

const texto = tipaEstado('');
const numero = tipaEstado(0);
const lista = tipaEstado([]);
console.log('estado de texto:', texto);
console.log('estado de numero:', numero);
console.log('estado de lista:', lista);
console.log('uma lista vazia sem o tipo do elemento nao ajuda o editor');

// (2) a interface de item e o que faz `map` valer a pena.
const itens = [
  { id: 'p41', nome: 'Bolo de cenoura', total: 90 },
  { id: 'p42', nome: 'Torta de limao', total: 45 },
  { id: 'p43', nome: 'Pao de queijo', total: 120 },
];

function nomesDaLista(lista) {
  return lista.map((item) => item.nome);
}
console.log('nomes:', nomesDaLista(itens));
console.log('total somado:', itens.reduce((soma, item) => soma + item.total, 0));

// (3) anotacao de retorno: a funcao e obrigada a devolver o que promete.
function resumoDoPedido(item) {
  return item.nome + ' (' + item.total + ')';
}
console.log('resumo de cada item:', itens.map(resumoDoPedido));

// (4) uniao: os estados validos, e nenhum estado invalido.
const ESTADOS = ['carregando', 'pronto', 'erro'];
function classifica(carregando, erro) {
  if (carregando) return ESTADOS[0];
  if (erro) return ESTADOS[2];
  return ESTADOS[1];
}
console.log('carregando ->', classifica(true, false));
console.log('com erro:   ->', classifica(false, true));
console.log('pronto:     ->', classifica(false, false));
console.log('a uniao impede que o estado seja um valor fora da lista');

// (5) generico: uma funcao para lista de qualquer tipo.
function primeiro(lista) {
  return lista.length > 0 ? lista[0] : undefined;
}
function totalDe(lista) {
  return lista.reduce((soma, item) => soma + item.total, 0);
}
console.log('primeiro item de uma lista de texto:', primeiro(['ana', 'bruno']));
console.log('primeiro item de uma lista de cliente:', primeiro(itens.map((i) => i.nome)));
console.log('total de uma lista vazia:', totalDe([]));
console.log('o mesmo corpo serve para tipos diferentes');

// (6) o resultado da busca e uma uniao: deu certo ou deu erro.
function busca(lista) {
  try {
    if (lista.length === 0) throw new Error('nenhum registro');
    return { ok: true, valor: lista };
  } catch (e) {
    return { ok: false, erro: e.message };
  }
}
const cheio = busca(itens);
const vazio = busca([]);
console.log('com dados:', cheio.ok, '| itens:', cheio.valor.length);
console.log('sem dados:', vazio.ok, '| erro:', vazio.erro);
console.log('ler `valor` sem conferir `ok` seria erro de tipo: e o ganho da uniao');

// (7) a arvore da lista tipada.
const estilos = StyleSheet.create({ item: { padding: 10 } });
function ItemTipado(props) {
  return <Text style={estilos.item}>{props.item.nome}</Text>;
}
function ListaTipada(props) {
  return (
    <View>
      {props.itens.map((item) => <ItemTipado key={item.id} item={item} />)}
    </View>
  );
}
const arvore = ListaTipada({ itens });
console.log('tipo:', arvore.type, '| filhos:', arvore.props.children.length);
console.log('cada filho recebeu o item inteiro:', arvore.props.children[1].props.children);
console.log('a `interface` de item atravessa a lista inteira a partir de um lugar so');

Saída real

estado de texto: { valor: '', tipo: 'string' }
estado de numero: { valor: 0, tipo: 'number' }
estado de lista: { valor: [], tipo: 'object' }
uma lista vazia sem o tipo do elemento nao ajuda o editor
nomes: [ 'Bolo de cenoura', 'Torta de limao', 'Pao de queijo' ]
total somado: 255
resumo de cada item: [
  'Bolo de cenoura (90)',
  'Torta de limao (45)',
  'Pao de queijo (120)'
]
carregando -> carregando
com erro:   -> erro
pronto:     -> pronto
a uniao impede que o estado seja um valor fora da lista
primeiro item de uma lista de texto: ana
primeiro item de uma lista de cliente: Bolo de cenoura
total de uma lista vazia: 0
o mesmo corpo serve para tipos diferentes
com dados: true | itens: 3
sem dados: false | erro: nenhum registro
ler `valor` sem conferir `ok` seria erro de tipo: e o ganho da uniao
tipo: View | filhos: 3
cada filho recebeu o item inteiro: Torta de limao
a `interface` de item atravessa a lista inteira a partir de um lugar so