Os Generics é uma forma de tipagem que permite que você indique o tipo da classe ou função no momento que for usar. Isso te da mais flexibilidade ao criar e usar tipos.
Funções
Vamos imaginar que temos uma simples função que recebe um argumento qualquer e retorna esse argumento.
function foo(arg) {
return arg;
}
Como estamos usando o typescript, vamos definir os tipos dessa função e do seu argumento. Mas, como podemos receber qualquer argumento a primeira coisa que poderiamos pensar é usar o tipo any
.
function foo(arg: any): any {
return arg;
}
Isso funciona, mas usar any
é como usar javascript, não vamos ter uma tipagem que nos ajude no desenvolvimento. Para deixar a função com uma boa tipagem e com flexibilidade no tipo, podemos usar o Generic.
Para usar o Generic numa função basta colocar <>
após o nome da função. E dentro do <>
definir o tipo genérico, geralmente usamos a letra T
(primeira letra de type). Feito isso é só usar o tipo genérico nos argumentos e/ou no retorno da função.
// Criamos um tipo T que será definido ao usar a função
function foo<T>(arg: T): T {
return arg;
}
foo<number>(20); // definindo o tipo T como number
foo<string>('20'); // definindo o tipo T como string
// O typescript é capaz de inferir o tipo T já que sabe o tipo do argumento
foo(20);
foo('20');
// Erro de compilação
foo<number>('20'); // espera um number e não uma string
foo<string>([20]); // espera uma string e não um array de number
Ao usar um tipo genérico, não estamos limitados a usar apenas um tipo. Vamos criar um nova função que irá criar um Map
. Um Map
é uma estrutura com chave (key) e valor (value). Então vamos ter dois tipos genéricos K
(primeira letra de key) e V
(primeira letra de value).
// Função genérica para criar um Map
function createMap<K, V>(): Map<K, V> {
return new Map<K, V>();
}
// Cria um Map com chaves do tipo string e valores do tipo number
const myMap = createMap<string, number>()
myMap.set("one", 1);
myMap.set("two", 2);
// Usando a inferencia de tipos do TypeScript
// O TypeScript infere o tipo da variável na função createMap
const otherMap: Map<number, number> = createMap();
otherMap.set(1, 100);
otherMap.set(2, 200);
// Erro de compilação:
myMap.set("three", [3]); // O valor deve ser um number e não um array de number
otherMap.set(3, [300]); // O valor deve ser um number
Classes
Assim como podemos ter funções genéricas, também podemos ter classes genéricas. Para isso basta colocar <>
após o nome da classe e definir os tipos genéricos dentro do <>
Pegando a inspiração do Java, vamos criar uma classe genérica List
para armazenar tipos genéricos.
// Definindo que a classe receberá um tipo genérico T
class List<T> {
private items: T[] = []; // Array para armazenar os itens do tipo T
add(item: T): void {
this.items.push(item);
}
get(index: number): T | undefined {
return this.items[index];
}
}
// Definindo o tipo T ao usar a classe
const ages = new List<number>(); // lista de idade
const names = new List<string>(); // lista de nomes
ages.add(18);
ages.add(22);
names.add("Alice");
names.add("Bob");
console.log(ages.get(0)); // 18
console.log(names.get(1)); // Bob
// Erro de compilação
names.add(30); // names só aceita strings
ages.add("Charlie"); // ages só aceita números
Em classes, não precisamos que toda a classe seja genérica, podemos apenas definir um método como genérico.
class MapFactory {
// Apenas o método é genérico
createMap<K, V>(): Map<K, V> {
return new Map<K, V>();
}
}
const mapFactory = new MapFactory();
const myMap = mapFactory.createMap<string, number>();
myMap.set("key1", 1);
// Usando inferência de tipo
const otherMap: Map<string, boolean> = mapFactory.createMap();
otherMap.set("notification", true);
otherMap.set("alert", false);
Genéricos Restritos
Há momentos que precisamos da flexibilidade de tipo do Generic, mas não flexível demais.
Vamos ver esse caso: precisamos criar uma função genérica, para aceitar vários tipos de objetos, mas esse objetos tem que ter o campo size
.
Para aceita vários tipos de objetos podemos usar o Generic ao criar a função.
function getSize<T>(obj: T): number {
// Erro de compilação: Property 'size' does not exist on type 'T'.
return obj.size;
}
O tipo genérico, como o nome já diz, é genérico! Então não sabemos se ele sempre terá o campo size: number
.
Pensando nesses problemas podemos restringir o tipo T
, tornando ele menos genérico para esse caso.
type SizeType = {
size: number;
};
function getSize<T extends SizeType>(obj: T): number {
// O compilador agora sabe que T possui o campo size
return obj.size;
}
Usando o extends
ao definir o tipo dizemos para o compilador que o tipo T
é um tipo genérico, mas ele também tem que ser do tipo SizeType
.
class MyArray {
size: number;
constructor(size: number) {
this.size = size;
}
}
class MyStack {
size: number;
constructor(size: number) {
this.size = size;
}
}
class User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
type SizeType = {
size: number;
};
function getSize<T extends SizeType>(obj: T): number {
return obj.size;
}
const myArray = new MyArray(10);
const myStack = new MyStack(5);
const user = new User("Alice", 30);
console.log(getSize(myArray)); // Output: 10
console.log(getSize(myStack)); // Output: 5
// Erro de compilação
// User não satisfaz o tipo SizeType para ser usado no getSize
console.log(getSize(user));
Outro caso que podemos usar genéricos restritos
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Vamos entender o que está acontecendo. Temos uma função que recebe dois tipos genéricos: T
e K
. O keyof
retorna todas as chaves de um tipo, então o K
está restrito as chaves do T
, ou seja, só aceita as chaves do objeto T
.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// retorna a propriedade name do objeto
getProperty({ name: "Foo", age: 30 }, "name"); // "Foo"
// retorna a propriedade 1 do objeto
getProperty({ 1: "Bar" }, 1); // "Bar"
// retorna a propriedade "size" de um objeto Map
getProperty(new Map([["name", "Foo"], ["age", "30"]]), "size"); // 2