Generics - TypeScript
TypeScript é uma linguagem fortemente tipada, mas, às vezes, é necessário construir funcionalidades que possam utilizar dados de qualquer tipo. Em alguns casos, poderíamos usar o tipo any
function getId(id: any): any {
return id;
}
let result = getId(5); // neste caso, result é do tipo any
console.log(result);
Entretanto, nesse caso, não podemos usar o resultado da função como um objeto do tipo que foi passado para a função; para nós, ele é do tipo any
function getId<T>(id: T): T {
return id;
}
Com a expressão <T>
getId
T
T
T
function getId<T>(id: T): T {
return id;
}
let result1 = getId<number>(5);
console.log(result1);
let result2 = getId<string>("abc");
console.log(result2);
No primeiro caso, o tipo number
T
string
T
Da mesma forma, podemos usar arrays genéricos:
function getString<T>(args: Array<T>): string {
return args.join(", ");
}
let result = getString<number>( [1, 2, 34, 5]);
console.log(result);
Nesse caso, independentemente do tipo de dados passados no array, a função irá retornar uma string.
Classes e Interfaces Genéricas
Além de funções e arrays genéricos, também existem classes e interfaces genéricas:
class User<T> {
private _id: T;
constructor(id:T) {
this._id = id;
}
getId(): T {
return this._id;
}
}
let tom = new User<number>(3);
console.log(tom.getId()); // retorna number
let alice = new User<string>("vsf");
console.log(alice.getId()); // retorna string
No entanto, nesse caso, é preciso considerar que, se tipificamos o objeto com um determinado tipo, não será possível alterar esse tipo posteriormente. Ou seja, no exemplo a seguir, a segunda criação do objeto não funcionará, pois o objeto tom
number
let tom = new User<number>(3);
console.log(tom.getId());
tom = new User<string>("vsf"); // erro
O mesmo vale para interfaces:
interface IUser<T> {
getId(): T;
}
class User<T> implements IUser<T> {
private _id: T;
constructor(id:T) {
this._id = id;
}
getId(): T {
return this._id;
}
}
Restringindo Generics
Os generics permitem trabalhar com qualquer tipo de dado. No entanto, às vezes é necessário usar não qualquer tipo, mas apenas um certo conjunto de tipos que atendam a determinados critérios. Por exemplo, consideremos a seguinte função:
function compareName<T>(obj1: T, obj2: T): void {
if (obj1.name === obj2.name) {
console.log("Nomes são iguais");
} else {
console.log("Nomes são diferentes");
}
}
A função recebe dois objetos cujo tipo é desconhecido. No código da função, são comparados os valores das propriedades name
Vamos tentar usar essa função para comparar dois objetos que possuem a propriedade name
let tom: { name: string } = { name: "Tom" };
let sam: { name: string } = { name: "Sam" };
compareName<{ name: string }>(tom, sam);
Aqui, estamos comparando dois objetos, tom
sam
{ name: string }
name
Ao chamar a função, compareName()
{ name: string }
Property 'name' does not exist on type 'T'
O uso de generics que se adequam a qualquer tipo amplia o conjunto de tipos utilizados, mas limita sua aplicação.
As restrições (constraints) permitem limitar o conjunto de tipos que podem ser usados nos generics. As restrições são definidas na forma:
<T extends CritérioDeTipos>
Após o nome do parâmetro (neste caso, T
extends
T
Por exemplo, no caso da função compareName()
name
function compareName<T extends { name: string }>(obj1: T, obj2: T): void {
if (obj1.name === obj2.name) {
console.log("Names are the same");
} else {
console.log("Names are different");
}
}
let tom: { name: string } = { name: "Tom" };
let sam: { name: string } = { name: "Sam" };
compareName<{ name: string }>(tom, sam);
A notação <T extends { name: string }>
Além disso, o parâmetro T
{ name: string }
function compareName<T extends { name: string }>(obj1: T, obj2: T): void {
if (obj1.name === obj2.name) {
console.log("Names are the same");
} else {
console.log("Names are different");
}
}
class User {
constructor(public name: string, public age: number) {}
}
let bob = new User("Bob", 38);
let bobic = new User("Bob", 24);
compareName<User>(bob, bobic);
type Person = { id: number; name: string };
let tom: Person = { id: 1, name: "Tom" };
let sam: Person = { id: 2, name: "Sam" };
compareName<Person>(tom, sam);
Aqui, na primeira chamada, a função compareName()
User
User
Person
{ id: number; name: string }
User
Person
name
{ name: string }
Além disso, qualquer tipo pode ser usado, por exemplo, interfaces:
interface Named {
name: string;
}
function compareName<T extends Named>(obj1: T, obj2: T): void {
if (obj1.name === obj2.name) {
console.log("Nomes são iguais");
} else {
console.log("Nomes são diferentes");
}
}
Da mesma forma, as restrições em generics podem ser aplicadas em interfaces e classes:
interface Named {
name: string;
}
class NameInfo<T extends Named> {
printName(obj: T): void {
console.log(`Name: ${obj.name}`);
}
}
class User {
constructor(public name: string, public age: number) {}
}
let bob = new User("Bob", 38);
let nameInfo1 = new NameInfo<User>();
nameInfo1.printName(bob);
type Person = { id: number; name: string };
let tom: Person = { id: 1, name: "Tom" };
let nameInfo2 = new NameInfo<Person>();
nameInfo2.printName(tom);
Nesse caso, a classe NameInfo
T
Named
Assim, podemos tipar objetos da classe NameInfo
name
User
Person
Usando a Palavra-chave new
Para criar um novo objeto em código genérico, precisamos indicar que o tipo genérico T
type: T
type: { new (): T }
function userFactory<T>(type: { new (): T }): T {
return new type();
}
class User {
constructor() {
console.log("Objeto User criado");
}
}
let user: User = userFactory(User);
Nesse caso, a função userFactory
type
T
T
T