Dia 10 — Componente com TypeScript
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
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