O que são os genéricos do TypeScript?
Os genéricos no TypeScript são um método para criar componentes ou funções reutilizáveis que podem lidar com vários tipos. Os genéricos são uma ferramenta poderosa que nos ajuda a criar funções reutilizáveis. Eles nos permitem construir estruturas de dados sem precisar definir um momento concreto para elas serem executadas em tempo de compilação.
Em TypeScript, eles servem ao mesmo propósito de escrever código reutilizável e seguro de tipo, onde o tipo da variável é conhecido em tempo de compilação. Isso significa que podemos definir dinamicamente o tipo de parâmetro ou função que será declarado antecipadamente. Isso é muito útil quando precisamos usar determinada lógica dentro de nossa aplicação; com essas peças reutilizáveis de lógica, podemos criar funções que recebem e entregam seus próprios tipos.
Podemos usar genéricos para implementar verificações em tempo de compilação, eliminar conversões de tipo e implementar funções genéricas adicionais em toda a nossa aplicação. Sem genéricos, o código de nossa aplicação compilaria em algum momento, mas pode não obtermos os resultados esperados, o que poderia levar bugs para a produção. Essa poderosa funcionalidade nos ajuda a criar classes, interfaces e funções reutilizáveis, genéricas e seguras de tipo.
Neste post, aprenderemos como alcançar segurança de tipo por meio de genéricos sem sacrificar desempenho ou eficiência. Podemos escrever um parâmetro de tipo entre colchetes angulares: <T>. Também podemos criar classes genéricas, métodos genéricos e funções genéricas no TypeScript.
Genéricos do TypeScript em ação
Funções sem usar genéricos
Consideremos o seguinte exemplo. Abaixo, temos uma função simples que irá produzir a ordem inversa do que fornecemos no array. Nomearemos nossa função removeRandomArrayItem:
function removeRandomArrayItem(arr: Array<number>): Array<number> { const randomIndex = Math.floor(Math.random() * arr.length); return arr.splice(randomIndex, 1); } removeRandomArrayItem([6, 7, 8, 4, 5, 6, 8, 9]);
A função acima nos diz que o nome da função é removeRandomArrayItem e que ela receberá um parâmetro de item, que é um tipo de array consistindo de números. Finalmente, esta função retorna um valor, que também é um array de números.
Como você pode ver, já introduzimos algumas restrições dentro do nosso código. Digamos que queira percorrer um array de números. Devemos construir outra função para lidar com este caso de uso?
Não! Aqui está o ponto forte onde os genéricos do TypeScript entram em ação.
Funções usando genéricos
Vamos dar uma olhada no problema antes de escrever nossa solução usando genéricos. Se passarmos a função acima um array de números, ela nos lançaria este erro:
Argument of type 'number[]' is not assignable to parameter of type 'string[]'
Podemos corrigir isso adicionando any à nossa declaração de tipo:
function removeRandomArrayItem(arr: Array<any>): Array<any> { const randomIndex = Math.floor(Math.random() * arr.length); return arr.splice(randomIndex, 1); } console.log(removeRandomArrayItem(['foo', 1349, 6969, 'bar']));
Mas não há motivo válido para usar o TypeScript se não estivermos lidando com tipos de dados. Vamos refatorar esta peça de função para convertê-la usando genéricos.
function removeRandomArrayItem<T>(arr: Array<T>): Array<T> { const randomIndex = Math.floor(Math.random() * arr.length); return arr.splice(randomIndex, 1); } console.log(removeRandomArrayItem(['foo', 'bar'])); console.log(removeRandomArrayItem([45345, 3453]));
Aqui, denotamos um tipo chamado <T>, que fará com que ele atue de forma mais genérica. Isso conterá o tipo de dados que é recebido pela própria função.
Classes do TypeScript usando genéricos
class Foo { items: Array<number> = []; add(item: number) { return this.items.push(item); } remove(item: Array<number>){ const randomIndex = Math.floor(Math.random() * item.length); return item.splice(randomIndex, 1); } } const bar = new Foo(); bar.add(22); bar.remove([1345, 45312613, 13453]);
Aqui, criamos uma classe simples chamada Foo, que contém uma variável que é um array de números. Também criamos dois métodos: um que adiciona itens ao array e outro que remove um elemento aleatório do array.
Esse pedaço de código funciona bem, mas introduzimos o mesmo problema de antes: se adicionarmos ou removemos itens no array, o genérico só aceitará array de números. Vamos refatorar esta classe para usar um genérico para aceitar um valor genérico, para que possamos passar qualquer tipo para o argumento.
class Foo<TypeOfFoo> { items: Array<TypeOfFoo> = []; add(item: TypeOfFoo) { return this.items.push(item); } remove(item: Array<TypeOfFoo>){ const randomIndex = Math.floor(Math.random() * item.length); return item.splice(randomIndex, 1); } } const bar = new Foo(); bar.add(22); bar.add('adfvafdv');
Com o uso de genéricos dentro de classes, tornamos nosso código muito mais reutilizável e sem repetições. É aqui que os genéricos realmente brilham!
Usando genéricos dentro de interfaces do TypeScript
Os genéricos não estão especificamente ligados a funções e classes. Também podemos usar genéricos no TypeScript dentro de uma interface. Vamos dar uma olhada em um exemplo de como podemos usá-lo em ação:
const currentlyLoggedIn = (obj: object): object => { let isOnline = true; return {...obj, online: isOnline}; } const user = currentlyLoggedIn({name: 'Ben', email: 'ben@mail.com'}); const currentStatus = user.online
Com as linhas acima escritas, obtemos um erro com uma linha ondulada nos avisando que não podemos acessar a propriedade de isOnline do usuário:
Property 'isOnline' does not exist on type 'object'.
Isso ocorre principalmente porque a função currentlyLoggedIn não conhece o tipo de objeto que está recebendo através do tipo de objeto que adicionamos ao parâmetro. Podemos resolver isso fazendo uso de um genérico:
const currentlyLoggedIn = <T extends object>(obj: T) => { let isOnline = true; return {...obj, online: isOnline}; } const user = currentlyLoggedIn({name: 'Ben', email: 'ben@mail.com'}); user.online = false;
A forma do objeto com o qual estamos lidando atualmente em nossa função pode ser definida em uma interface:
interface User<T> { name: string; email: string; online: boolean; skills: T; } const newUser: User<string[]> = { name: "Ben", email: "ben@mail.com", online: false, skills: ["foo", "bar"], }; const brandNewUser: User<number[]> = { name: "Ben", email: "ben@mail.com", online: false, skills: [2456234, 243534], };
Aqui está outro exemplo de como podemos usar uma interface com genéricos. Definimos uma interface com Greet, que vai receber um genérico, e esse genérico específico será o tipo que será passado para a propriedade skills.
Com isso, podemos passar o valor desejado para a propriedade skills em nossa função Greet.
interface Greet<T> { fullName: "Ben Douglass"; skills: T messageGreet: string } const messageGreetings = (obj: Greet<string>): Greet<string> => { return { ...obj, messageGreet: `${obj.fullName} welcome to the app`, skills: 'sd' }; };
Passando valores genéricos padrão para genéricos
Também podemos passar um tipo genérico padrão para o nosso genérico. Isso é útil em casos em que não queremos passar o tipo de dados com o qual estamos lidando em nossa função de forma obrigatória. Por padrão, estamos definindo como um número.
function removeRandomArrayItem<T = number>(arr: Array<T>): Array<T> { const randomIndex = Math.floor(Math.random() * arr.length); return arr.splice(randomIndex, 1); } console.log(removeRandomArrayItem([45345, 3453, 356753, 3562345, 3567235]));
Este trecho reflete como fizemos uso do tipo genérico padrão em nossa função removeRandomArray. Com isso, somos capazes de passar um tipo genérico padrão de número.
Passando múltiplos valores genéricos
Se quisermos que nossos blocos reutilizáveis de funções aceitem múltiplos genéricos, podemos fazer o seguinte:
function removeRandomAndMultiply<T = string, Y = number>(arr: Array<T>, multiply: Y): [T[], Y] { const randomIndex = Math.floor(Math.random() * arr.length); const multipliedVal = arr.splice(randomIndex, 1); return [multipliedVal, multiply]; } console.log(removeRandomAndMultiply([45345, 3453, 356753, 3562345, 3567235], 608));
Aqui, criamos uma versão modificada da nossa função anterior para que possamos introduzir outro parâmetro genérico. Denotamos com a letra Y, que está definido como um tipo padrão de número, pois irá multiplicar o número aleatório que escolhemos do array dado.
Como estamos multiplicando números, certamente estamos lidando com um tipo de número, então podemos passar o tipo genérico padrão de número.
Adicionando restrições aos genéricos
Os genéricos nos permitem trabalhar com quaisquer tipos de dados que são passados como argumentos. No entanto, podemos adicionar restrições ao genérico para limitá-lo a um tipo específico.
Um parâmetro de tipo pode ser declarado que é limitado por outro parâmetro de tipo. Isso nos ajudará a adicionar restrições sobre o objeto, garantindo que não obtenhamos uma propriedade que possivelmente não existe.
function getObjProperty<Type, Key extends keyof Type>(obj: Type, key: Key) { return obj[key]; } let x = { name: "Ben", address: "New York", phone: 7245624534534, admin: false }; getObjProperty(x, "name"); getObjProperty(x, "admin"); getObjProperty(x, "loggedIn"); //property doesn't exist
No exemplo acima, criamos uma restrição para o segundo parâmetro que a função recebe. Podemos invocar esta função com os respectivos argumentos e tudo funcionará, a menos que passemos um nome de propriedade que não existe no tipo de objeto com o valor de x. Este é o modo como podemos restringir as propriedades de definição de objetos usando genéricos.
Conclusão
Neste post, exploramos como podemos usar genéricos e criar funções reutilizáveis dentro de nossa base de código. Implementamos genéricos para criar uma função, classe, interface, método, múltiplas interfaces e genéricos padrão.