Os genéricos fornecem partes reutilizáveis de código que funcionam com vários tipos em vez de um único tipo. Os genéricos permitem tratar o tipo como uma variável e especificá-lo aquando da sua utilização, à semelhança dos parâmetros das funções.
Os genéricos podem ser utilizados em conjunto com funções (criando uma função genérica), classes (criando uma classe genérica) e interfaces (criando uma interface genérica).
Utilização básica
Provavelmente já utilizou genéricos no passado, mesmo sem o saber - a utilização mais comum dos genéricos é declarar uma matriz:
const myArray: string[];
Não é muito especial à primeira vista, apenas declaramos minhaVariedade como um array de strings, mas é o mesmo que uma declaração genérica:
const myArray: Array;
Manter as coisas explícitas
Vamos começar com um exemplo muito simples - como poderíamos portar esta função JS para TypeScript:
function getPrefiledArray(filler, length) {
return (new Array(length)).fill(filler);
}
Esta função devolverá uma matriz preenchida com uma determinada quantidade de enchimento, portanto comprimento será número e toda a função devolverá uma matriz de enchimento - mas o que é um enchimento? Nesta altura, pode ser qualquer coisa, por isso uma opção é utilizar qualquer:
function getPrefiledArray(filler: any, length: number): any[] {
return (new Array(length)).fill(filler);
}
Utilizar qualquer é certamente genérico - podemos passar literalmente qualquer coisa, por isso "trabalhar com um número de tipos em vez de um único tipo" da definição está totalmente coberto, mas perdemos a ligação entre enchimento e o tipo de retorno. Neste caso, queremos devolver uma coisa comum, e podemos definir explicitamente esta coisa comum como parâmetro de tipo:
function getPrefiledArray(filler: T, length: number): T[] {
return (new Array(length)).fill(filler);
}
e utilizar desta forma:
const prefilledArray = getPrefiledArray(0, 10);
Restrições genéricas
Vejamos outros casos, provavelmente mais comuns. Porque é que usamos tipos em funções? Para mim, é para garantir que os argumentos passados para a função têm algumas propriedades com as quais quero interagir.
Mais uma vez vamos tentar portar uma função simples do JS para o TS.
function getLength(thing) {
return thing.length;
}
Temos um enigma não trivial - como garantir que a coisa tem um comprimento e o primeiro pensamento pode ser fazer algo do género:
function getLength(thing: typeof Array):number {
return thing.length;
}
e, dependendo do contexto, pode estar correto, mas em geral somos um pouco genéricos - funciona com matrizes de vários tipos, mas e se não soubermos se a coisa deve ser sempre uma matriz - talvez a coisa seja um campo de futebol ou uma casca de banana? Neste caso, temos de reunir as propriedades comuns dessa coisa numa construção que possa definir as propriedades de um objeto - uma interface:
interface IThingWithLength {
comprimento: número;
}
Podemos utilizar IThingWithLength como tipo da interface coisa parâmetro:
function getLength(thing: IThingWithLength):number {
return thing.length;
}
francamente, neste exemplo simples, não haverá problema algum, mas se quisermos manter este tipo genérico e não enfrentar o problema do primeiro exemplo, podemos usar Restrições genéricas:
function getLength(thing: T):number {
return thing.length;
}
e utilizá-lo:
interface IBananaPeel {
espessura: número;
comprimento: número;
}
const bananaPeel: IBananaPeel = {thickness: 0.2, length: 3.14};
getLength(bananaPeel);
Utilizar estende-se garante que T conterá propriedades que são definidas por IThingWithLength.
Classes genéricas
Até agora, estávamos a trabalhar com funções genéricas, mas não é o único sítio onde os genéricos brilham, vamos ver como os podemos incorporar nas classes.
Em primeiro lugar, vamos tentar guardar um cacho de bananas no cesto das bananas:
classe Banana {
construtor(
public comprimento: número,
public cor: string,
public ionizingRadiation: number
) {}
}
class BananaBasket {
private bananas: Banana[] = [];
add(banana: Banana): void {
this.bananas.push(banana);
}
}
const bananaBasket = new BananaBasket();
bananaBasket.add(new Banana(3.14, 'red', 10e-7));
Agora vamos tentar criar um cesto de uso geral, para coisas diferentes com o mesmo tipo:
class Basket {
private stuff: T[] = [];
add(thing: T): void {
this.stuff.push(thing);
}
}
const bananaBasket = new Basket();
E, por último, vamos supor que o nosso cesto é um contentor de material radioativo e que só podemos armazenar matéria que tenha radiação ionizante propriedade:
interface IRadioactiva {
ionizingRadiation: número;
}
class RadioactiveContainer {
private stuff: T[] = [];
add(thing: T): void {
this.stuff.push(thing);
}
}
Interface genérica
Por fim, vamos tentar reunir todos os nossos conhecimentos e construir um império radioativo utilizando também Interfaces Genéricas:
// Definir atributos comuns para os contentores
interface IRadioactive {
ionizingRadiation: number;
}
// Definir algo que é radioativo
interface IBanana extends IRadioactive {
comprimento: número;
cor: string;
}
// Definir algo que não é radioativo
interface IDog {
peso: número;
}
// Definir interface para um contentor que só pode conter coisas radioactivas
interface IRadioactiveContainer {
add(thing: T): void;
getRadioactiveness():number;
}
// Definir a classe que implementa a interface do contentor radioativo
class RadioactiveContainer implements IRadioactiveContainer {
private stuff: T[] = [];
add(thing: T): void {
this.stuff.push(thing);
}
getRadioactiveness(): número {
return this.stuff.reduce((a, b) => a + b.ionizingRadiation, 0)
}
}
// ERRO! O tipo 'IDog' não satisfaz a restrição 'IRadioactive'
// E é um pouco brutal guardar cães dentro de um contentor radioativo
const dogsContainer = new RadioactiveContainer();
// Tudo bem, amigo!
const radioactiveContainer = new RadioactiveContainer();
// Lembra-te de separar os teus resíduos radioactivos - cria um contentor separado só para bananas
const bananasContainer = new RadioactiveContainer();
É tudo, pessoal!