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. 😉