Generics Typescript

generics-typescript

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
Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
proven-hbk-force-measurement-technology

Proven HBK Force Measurement Technology

Next Post
the-2-rules-of-sponsored-content-you’re-not-allowed-to-forget

The 2 Rules of Sponsored Content You’re Not Allowed to Forget

Related Posts