TypeScript: o que são tipos genéricos?

Subscribe to my newsletter and never miss my upcoming articles

Após algum tempo trabalhando com a linguagem TypeScript, se aventurando em códigos de outros programadores e mesmo utilizando alguns famosos frameworks, você pode ter se deparado com trechos de código semelhantes a este...

interface Collection<T> {...}

...e pensado: afinal, o que é este T? Como utilizá-lo em meu código?

Bem, o primeiro ponto a ser considerado é que no lugar deste T poderia haver qualquer outra letra ou nome. Se eu quiser fazer isto, eu posso:

interface Collection<ModelType> {...}

Entretanto, por convenção, quando criamos um tipo genérico, é comum referenciarmos ele apenas pela letra T pois, no exemplo acima, ModelType mais parece um tipo já existente que um tipo genérico exclusivo à interface onde está sendo utilizado.

Agora vamos ver como utilizar este recurso da TypeScript.

O que um tipo genérico representa, exatamente?

Vamos supor que você deseja criar uma função que receberá um argumento de qualquer tipo e retornará um valor deste mesmo tipo. Talvez a solução que lhe venha à mente seja esta:

function doSomething(arg: any): any {...}

Mas preste atenção a um detalhe: ao mesmo tempo em que você está dizendo que pode receber qualquer tipo de valor como argumento, também está definindo que qualquer tipo de valor pode ser retornado!

Ou seja, eu posso passar uma string como argumento e receber um valor de tipo number, object, array ou qualquer outra coisa. Mas, no nosso exemplo, não é isso o que queremos. Esperamos receber um tipo de valor igual ao que passamos. Qual a solução para isso?

function doSomething<T>(arg: T): T {...}

No código acima, criamos um tipo genérico T e definimos que tanto o argumento quanto o retorno da função serão deste tipo. T, aqui, representa qualquer tipo de valor. No entanto, uma vez definido este tipo, ele não pode mais ser alterado. Isto quer dizer que quando inserirmos um argumento de tipo string o interpretador automaticamente saberá que o retorno da função será uma string também.

Quando utilizar?

Há diferentes modos de usar tipos genéricos. O importante, portanto, é saber se são necessários ou não.

Tipos genéricos são úteis quando você deseja garantir que, ao mesmo tempo que seu código tenha a versatilidade para trabalhar com qualquer tipo de valor, ele também se atenha somente a este, para evitar confusões.

Imagine, por exemplo, que você deseja criar uma função que faça requisições PATCH ao servidor, enviando apenas partes de instâncias de modelos que existem em seu código. Neste caso, uma mesma função irá trabalhar com diferentes tipos de objetos. Assim, faria sentido utilizar o seguinte código:

type Partial<T> = {
  [P in keyof T]?: T[P];
};

function updateInstance<T>(obj: Partial<T>, id: number): Partial<T> {...}

Primeiro, criamos um tipo chamado Partial, que representa parte de um objeto qualquer (T). Apenas para exemplificar, poderíamos fazer isso:

interface Product {
  id?: number;
  name: string;
  price: number;
}

let partialProduct: Partial<Product> = { price: 19 }

Depois, criamos uma função com um tipo genérico, que será utilizado para determinarmos de que tipo será o objeto parcial que passamos como argumento.

Não poderíamos fazer isto, por exemplo:

updateInstance<Product>({ anotherProperty: 'testing' }, 2); // error

O compilador nos mostraria um erro, pois anotherProperty não existe no tipo Product.

Mas, se fizermos isto, não haverá problema:

updateInstance<Product>({ name: 'Computer' }, 2); // ok

Mais exemplos

Veja alguns outros modos de utilizar tipos genéricos.

Em interfaces

Neste caso, uma view que poderia renderizar uma lista de objetos de um determinado tipo, dando ao usuário a opção de selecionar um deles.

interface ContentView<T> {
  itemsList: T[];
  selectedItem: T;
}

Em classes

Aqui, uma classe que nos permitiria fazer requisições para o servidor baseando-se em um dado tipo, como Product, User ou Post.

class Request<T> {
  constructor() { }

  get(): T[] {...}
  getById(id: number): T {...}
  post(obj: T): T {...}
  put(obj: T, id: number): T {...}
}

Em funções

Uma função que trabalharia com dois tipos genéricos diferentes para seus argumentos.

function doSomething<T1, T2>(arg1: T1, arg2: T2): void {...}

Conclusão

Utilize tipos genéricos quando estiver disposto a aceitar qualquer tipo, ao passo que uma vez definido este, seu código aceite trabalhar somente com ele.

Recomendo fortemente que leia a documentação sobre tipos genéricos no site da TypeScript. Lá, você verá mais exemplos que irão reforçar seus conhecimentos e irá aprender também a utilizar tipos avançados.

Se este artigo lhe ajudou, me deixe saber nos comentários. 😉

No Comments Yet