O problema que generics resolve
Duas funções quase idênticas, uma para string[] e outra para number[], ou uma única função com any[] que perde toda a informação de tipo. Nenhuma das duas é boa.
Estudar esta lição interativa →Imagine uma função primeiro, que devolve o primeiro item de um array. Parece simples até você tentar tipá-la de um jeito que funcione tanto para uma lista de nomes quanto para uma lista de preços. Uma primeira tentativa, tipando o parâmetro como any[], até roda, mas olha o que ela perde:
// any[] "resolve", mas apaga a informação de tipo do retorno.
function primeiroAny(itens: any[]) {
return itens[0]; // o retorno também vira any
}
const nomesAny = ["Ana", "Bruno", "Carla"];
const precosAny = [19.9, 29.9, 39.9];
console.log(primeiroAny(nomesAny).toUpperCase()); // "funciona", sem garantia real
console.log(primeiroAny(precosAny).toUpperCase()); // erro só em tempo de execução, sem aviso aquiRepare no comentário: o retorno virou any, sem nenhuma proteção.
Com any[], o retorno da função também vira any, porque o TypeScript não tem como saber, de antemão, que tipo de item está ali dentro. Isso significa que primeiro(nomes).toUpperCase() funciona, mas primeiro(precos).toUpperCase() também "funciona" aos olhos do compilador (e quebra ao rodar, já que number não tem esse método). Você perdeu exatamente a proteção que veio buscar neste curso, igual ao any solto do Módulo 2.
O que a gente realmente quer dizer é: "essa função aceita um array de qualquer tipo, mas o item que ela devolve é do mesmo tipo dos itens daquele array específico". Essa relação entre a entrada e a saída, "seja lá qual for o tipo, o retorno é esse mesmo tipo", é exatamente o que um generic (tipo genérico) expressa, com uma letra entre <> logo depois do nome da função:
// Generic <T>: o mesmo T aparece no parâmetro e no retorno, preservando o tipo real.
function primeiro<T>(itens: T[]): T {
return itens[0];
}
const nomesGenerico = ["Ana", "Bruno", "Carla"];
const precosGenerico = [19.9, 29.9, 39.9];
console.log(primeiro(nomesGenerico).toUpperCase()); // T virou string aqui
// console.log(primeiro(precosGenerico).toUpperCase()); // erro: T virou number aqui, sem toUpperCase
console.log(primeiro(precosGenerico).toFixed(2));T se ajusta ao tipo real de cada chamada, sem precisar de duas funções.
Pense num generic como um recipiente vazio com uma etiqueta escrita a lápis: "T". Quando você despeja nomes dentro (chama primeiro com um array de string), a etiqueta se preenche com "string" só para aquela vez. Quando despeja preços (chama primeiro com um array de number), a mesma etiqueta, no mesmo recipiente, se preenche com "number" dessa vez. O recipiente é um só, reaproveitável, mas o conteúdo declarado muda a cada uso, sem nunca virar "qualquer coisa" como o any faria.
A letra T não tem nenhum significado especial embutido na linguagem, é só a convenção mais comum (de "Type"). Você poderia escrever <Item> ou <Valor> e o comportamento seria idêntico. Em generics com mais de um parâmetro de tipo, é comum ver <T, U> ou nomes descritivos, como <Entrada, Saida>.
No exemplo, primeiro([]) devolveria undefined, mas o tipo promete T. Com o editor deste curso isso passa, mas num projeto criado com tsc --init nas versões recentes, o noUncheckedIndexedAccess vem ligado e return itens[0] vira erro (Type 'T | undefined' is not assignable to type 'T'). A assinatura honesta é primeiro<T>(itens: T[]): T | undefined.
// Sua vez! Complete a função genérica "ultimo", que devolve o último item
// de um array de qualquer tipo (dica: itens[itens.length - 1]).
function ultimo<T>(itens: T[]): T {
// complete aqui
}
const nomesPratica = ["Ana", "Bruno", "Carla"];
const precosPratica = [19.9, 29.9, 39.9];
console.log(ultimo(nomesPratica));
console.log(ultimo(precosPratica));Complete a função genérica ultimo e teste com os dois arrays.