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. Se, em vez do número 5, fosse passado um objeto de alguma classe e, posteriormente, precisássemos usar esse objeto, por exemplo, chamando seus métodos, isso seria problemático. Para especificar o tipo de retorno, podemos usar generics:

function getId<T>(id: T): T {
    return id;
}

Com a expressão <T>, indicamos que a função getId é tipada com um certo tipo T(pode ser qualquer letra, mas geralmente é usada a letra T). No momento em que escrevemos a função, podemos não saber qual será esse tipo. Ao chamar a função, um tipo concreto substituirá T. Assim, a função retornará um objeto desse tipo. Por exemplo:

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 será usado em vez do parâmetro T, permitindo-nos passar um número para a função. No segundo caso, o tipo string é usado em vez de T, permitindo passar uma string. Dessa forma, podemos passar para a função objetos de diferentes tipos, mas mantendo a tipagem forte; cada variante da função genérica pode aceitar objetos apenas de um tipo específico.

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 já está tipado como 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 desses objetos.

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 e sam, que têm o mesmo tipo { name: string }. Ou seja, ambos os objetos possuem a propriedade name.

Ao chamar a função, compareName() é tipada com esse tipo { name: string }. Pareceria que não deveria haver problemas. No entanto, ao compilar, receberemos o erro:

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), vem a palavra-chave extends, seguida pelo critério que os tipos de dados devem atender ao serem passados no lugar do parâmetro T.

Por exemplo, no caso da função compareName() do exemplo acima, os tipos devem ter a propriedade name. Portanto, vamos reescrever a função da seguinte maneira:

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 }> significa que o parâmetro T deve representar um tipo que contenha a propriedade name, como no caso dos objetos tom e sam.

Além disso, o parâmetro T não precisa necessariamente representar exatamente o tipo { name: string }. Por exemplo:

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() é tipada com a classe User, ou seja, os objetos passados para ela devem ser instâncias da classe User. Na segunda chamada, a função é tipada com o tipo Person, que representa o objeto { id: number; name: string }. Tanto User quanto Person têm em comum a propriedade name e, portanto, atendem à restrição { 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 utiliza um parâmetro de tipo T, que é restrito pela interface Named.

Assim, podemos tipar objetos da classe NameInfo com qualquer tipo que tenha a propriedade name, como a classe User ou o tipo Person.

Usando a Palavra-chave new

Para criar um novo objeto em código genérico, precisamos indicar que o tipo genérico T possui um construtor. Isso significa que, em vez do parâmetro type: T, precisamos especificar type: { new (): T }. Por exemplo:

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 utiliza um parâmetro type que deve ser um construtor de T. Assim, podemos criar novas instâncias do tipo genérico T especificando que T possui um construtor.

Política de Privacidade

Copyright © www.programicio.com Todos os direitos reservados

É proibida a reprodução do conteúdo desta página sem autorização prévia do autor.

Contato: programicio@gmail.com