Este post é o sexto de uma série de 6 posts sobre TypeScript. Veja os detalhes dos posts aqui.

Post anterior: TypeScript – Módulos

Neste post, vamos aprender como trabalhar com generics no TypeScript. O generic nos permite reutilizar uma determinada implementação de código, de forma tipada. Tonando um código mais seguro. Vamos começar com um exemplo bem simples, apenas para entender a forma como declarar um generic no TypeScript.

function getValue<T>(arg: T): T {
    return arg;
}

window.onload = () => {
    var value: string = getValue<string>("Hello");
    console.log(value);
};

No exemplo acima, temos uma função chamada “getValue”, ela por sua vez, declara um generic chamado “T” (utilizamos comummente “T”, para denotar a palava “type”), fica entre “<>”. A mesma função, exige um argumento, cujo o tipo deve ser do mesmo tipo que o generic “T”, ou seja, qualquer que seja o tipo informado (será informado no momento que a função for invocada), a função irá exigir o mesmo tipo informado. Ainda na função, o retorno da função, também deve ser do mesmo tipo de do generic “T”. No corpo da função, apenas retornamos o valor, que foi passado por parâmetro na função. A função em si não tem utilidade, serve apenas para demostrar de forma simples e didática, como trabalhar com generic no TypeScript.

Abaixo temos uma variável chamada “value”, cujo o tipo é “string” (nesse momento, já devemos subentender que o generic deve ser “string” também).
Atribuímos a variável “value”, o retorno da função “getValue”. Passamos uma “string” como argumento da função. E por fim, “imprimimos” o valor da variável “value” no console do navegador.
Qualquer outro tipo primitivo poderia ser utilizado: “number”, “boolean”, “string”, etc.

Generic Type com Arrow Function

O TypeScript da condição, de declarar generic type, para trabalhar com “arrow function”, vamos criar um exemplo simples.

function getValue<T>(arg: T): T {
    return arg;
}

window.onload = () => {
    var value: <T>(arg: T) => T;
    value = getValue;
    console.log(value<string>("Hello"));
};

No exemplo acima, temos a mesma função do primeiro exemplo.
Abaixo, declaramos uma variável chamada “value”, cujo o tipo é um generic chamado “T”, que deve ter como argumento um “T” também, e retornar um “T”, ou seja, ao definir o valor da variável, devemos atribuir uma “function type” que respeita a sua assinatura e seu retorno. Abaixo, atribuímos o valor da variável “value”, com a função “getValue”. E por fim, “imprimimos” o valor de retorno da variável “value” no console do navegador.

Generic Constraints

O TypeScript permite definir restrições (constraints), para delimitar um determinado tipo como base. Isso ajuda na abstração e reutilização, já que a partir daí temos como base uma classe ou interface que conhecemos sua estrutura. Vamos criar um exemplo mais elaborado.
A ideia é criar uma abstração, de uma classe que gere uma tabela html, assim será possível nos preocupar apenas com os objetos. E uma classe ficará responsável por gerar o HTML. Este exemplo é apenas didático, em um caso real, utilizaria um framework MVVM, não geraria o html no TypeScript ou JavaScript. É apenas um exemplo simples.

Antes de começar, vamos levantar o que sabemos. Sabemos que a tabela é composta por linhas. Esta será representada por cada instancia de um objecto. Também sabemos que cada instancia deverá ter um identificador único, ou seja, cada instancia terá uma propriedade “id” (poderia ser outro nome). Sendo assim, vamos conseguir parametrizar se a tabela pode remover ou editar o registro.

class Entity {
    public id: number;
    public isDeleted: boolean;

    constructor(id: number, isDeleted: boolean = false) {
        this.id = id;
        this.isDeleted = isDeleted;
    }
}

class Product extends Entity {
    public name: string;
    public price: number;

    constructor(id: number, name: string, price: number, isDeleted: boolean = false) {
        super(id, isDeleted);
        this.name = name;
        this.price = price;
    }
}

class ListingUI<TEntity extends Entity> {
    public data: Array<TEntity>;
    public nameResource: string;
    public allowRemove: boolean;
    public allowEdit: boolean;

    constructor(data: Array<TEntity>, nameResource: string, allowRemove: boolean = true, allowEdit: boolean = true) {
        this.data = data;
        this.nameResource = nameResource;
        this.allowRemove = allowRemove;
        this.allowEdit = allowEdit;
    }

    public gerateHtml(): string {
        if (this.data.length == 0)
            return "";

        var html: string = "
<table border='1' cellspacing='2' celladding='2'>
<thead>
<tr>";
        for (var propertyName in this.data[0]) {
            if (propertyName != "isDeleted" && propertyName != "constructor")
                html += "
<th>" + propertyName + "</th>

";
        }

        if (this.allowEdit || this.allowRemove)
            html += "
<th>Ações</th>

";

        html += "</tr>

" +
        "</thead>

" +
        "
<tbody>";

        for (var indexRow in this.data) {
            var row = this.data[indexRow];
            var urlRemove = "/" + this.nameResource + "/delete/" + row.id;
            var urlEdit = "/" + this.nameResource + "/edit/" + row.id;

            if (row.isDeleted)
                continue;

            html += "
<tr>";
            for (var propertyName in row) {
                var rowValue = row[propertyName];
                if (typeof (rowValue) != "function" && propertyName != "isDeleted")
                    html += "
<td>" + rowValue + "</td>

";
            }

            if (this.allowEdit || this.allowRemove)
                html += "
<td>";

            if (this.allowRemove)
                html += "<a href='#" + urlRemove + "'>Remover</a>";

            if (this.allowEdit)
                html += "&nbsp;<a href='#" + urlEdit + "'>Editar</a>";

            if (this.allowEdit || this.allowRemove)
                html += "</td>

";

            html += "</tr>

 ";
        }

        html += "</tbody>

" +
        "</table>

";

        return html;
    }
}

window.onload = () => {
    var products: Array<Product> = [
        new Product(1, "Mesa", 100.10),
        new Product(2, "Cadeira", 40.10),
        new Product(3, "Descanso de panela", 9.99),
        new Product(4, "Jogo de talheres", 46.78, true)
    ];

    var tableProducts: ListingUI<Product> = new ListingUI<Product>(products, "Product");
    var htmlTableProducts: string = tableProducts.gerateHtml();
    document.body.innerHTML = htmlTableProducts;
};

No exemplo acima, temos uma classe chamada “Entity”, ela deverá servir de base para qualquer classe que será utilizada em nosso “gerador de tabela html”. Ela possui duas propriedades. “id” do tipo “number”, e uma propriedade “isDeleted” do tipo “boolean”. A classe também possui um construtor, que tem os argumentos para preencher as propriedades da classe. Nesse ponto sabemos o que toda entidade tem em comum, já que esta servirá como herdeira.

Também temos uma classe chamada “Product”, que é herdeira de “Entity”. Ela tem duas propriedades, “name” do tipo “string”, e “price” do tipo “number”. O construtor tem os mesmos argumentos da classe base, alem dos seus, para definir os valores de suas propriedades.

A classe “ListingUI”, define um generic com o nome “TEntity”, que por sua vez, restringe que a classe que for passada por parâmetro generic, deve ser herdeira ou a própria classe “Entity”. Especificamos isso através da palavra chave “extends”. Esta palavra chave, é usada tanto para definir herança de classe, como definir uma constraint de classe. Ainda nessa classe, temos algumas propriedades.

* data: Esta propriedade, é do tipo “Array”. É um array do tipo generic, que é informado via parâmetro generic da classe, ou seja, pode ser qualquer classe, desde de que seja herdeiro de “Entity”, ou a própria classe “Entity”. Ela armazenará as instancias, que representará as linhas da tabela.
* nameResource: Está propriedade é do tipo “string”. Servirá para armazenar o nome do recurso que utilizaremos para criar a url de editar e excluir.
* allowRemove e allowEdit: Ambas são do tipo “boolean”, servirá para informar se a tabela deve ou não exibir os links “editar” e/ou remover.

Ainda nesta classe, temos um construtor, que define os valores das propriedades. Repare nos valores padrões.

E por último, temos o método “gerateHtml”, ele deverá gerar a tabela, no formato HTML.
Verificamos se o array “data” tem mais de um item, caso não tenha, o algoritmo para por ali.
Declaramos um variável com o nome “html”, do tipo “string”, com um valor inicial, que nada mais é do que a abertura de uma tag “table”.
Em seguida, percorremos todos os membros da estrutura da classe. Para isso, utilizamos a primeira instancia de “data”, como amostra. Assim é possível definir o “thead” da tabela. Verificamos se o membro é diferente de “isDeleted” e “constructor”, não queremos adicionar eles no “thead”.
Abaixo, verificamos se as propriedades “allowEdit” ou “allowRemove”, estão como “true”, caso sim, é adicionado uma outra coluna, com o título “Ações”.
Depois, percorremos os dados do array “data”. Basicamente fazemos o mesmo que acima, porem, adicionamos os dados, em vez dos nomes de propriedade.

Por fim, fizemos uso das classe que criamos. Criando uma instancia com o nome “products”, do tipo “Array”, com valores padrões. Repare que a última instancia está marcada como excluída. Declaramos uma variável “tableProducts” do tipo “ListingUI”. Invocamos o método “gerateHtml”, para atribuir o valor para variável “htmlTableProducts” do tipo “string”. E “imprimimos” o valor no console do navegador.

Espero ter ajudado!
Até a próxima!