🧙♂️ Typescript Generics - O que é, porque existe e como utilizar
Entendendo como surgiu o Generics
Digamos que você não sabe nada sobre Generics e você quer criar uma função para verificar se dois números são iguais e caso não sejam, estourar um error.
Você poderia implementar essa função da seguinte forma:
const numMustBeEqual = (x: number, y: number) => {
if (x !== y) throw new Error(`${x} isn't equal to ${y}`)
}
Mas digamos que depois de um também, você quer fazer o mesmo, só que com strings, então você poderia fazer algo como:
const numMustBeEqual = (x: number, y: number) => {
if (x !== y) {
throw new Error(`${x} isn't equal to ${y}`);
}
};
const strMustBeEqual = (x: string, y: string) => {
if (x !== y) {
throw new Error(`${x} isn't equal to ${y}`);
}
};
Mas criar duas funções que fazem a mesma coisa, apenas para poder receber tipos diferentes, não é algo muito bom.
Então você decide usar any
para poder receber qualquer tipo.
const anyMustBeEqual = (x: any, y: any) => {
if (x !== y) {
throw new Error(`${x} isn't equal to ${y}`);
}
};
Mas usar any
não é uma boa pratica e também pode permitir com que se faça coisas como
anyMustBeEqual(123, "abc");
Onde se comprara strings com números, que claramente não são iguais.
E é ai que você percebe a necessidade de usar Generics.
Você quer fazer uma função que possa ser utilizada com mais de um tipo, mas sem especificar-lo, tendo parâmetros que são genéricos.
Utilizando Generics
Para utilizarmos um tipo genérico em nossa função, passaremos um parâmetro de tipo utilizando <
e >
, e dentro passaremos o nome do tipo que queremos.
const mustBeEqual = <T>(x: T, y: T) => {
if (x !== y) {
throw new Error(`${x} isn't equal to ${y}`);
}
};
Você pode visualizar uma função genérica como tendo dois tipos de parâmetros. Parâmetros que se referem a um tipo (generics) e que se referem a uma variável.
Da mesma forma que você você criar um parâmetro em uma função utilizando (
+ nome da variável + )
, com parâmetros de tipo, utilizamos <
+ nome do tipo + >
. Portanto T
poderia ser qualquer outro nome.
Você também pode passar quantos parâmetros do tipo você quiser, utilizando ,
entre eles, pode passar valores default usando =
e utilizar outros parâmetros também
const func = <T, V = number, K = V[]>(foo: T, bar: V, baz: K) => {}
Generics é usado não só em funções, mas em classes, interfaces e tipos. Para criar uma interface genérica, basta passar os parâmetros de tipo logo após o nome e utiliza-los como um tipo dentro da função.
interface Data<T> {
id: T;
}
Um exemplo para utilizar essa interface genérica seria fazer uma função que filtra um array de Data<T>
através de seu id
const filterById = <T>(id: T, data: Data<T>[]) => {
return data.filter((x) => x.id !== id);
};
Utilizando Type Constraints
Essa função tem um pequeno detalhe, ela permite que se use id
de qualquer tipo, podendo ser até mesmo um objeto ou um array, mas o id
deve ser apenas string
ou number
.
Para resolver esse problema, precisamos definir que data
apenas estenda a interface e não que seja idêntica a ela. Para isso, criaremos um novo parâmetro de tipo e definiremos uma restrição (type constraint) usando extends
que restringirá esse tipo para extender Data<T>
.
interface Data<T extends string | number> {
id: T;
}
const filterByIdExtended = <T extends string | number>(id: T, data: Data<T>[]) => {
return data.filter((x) => x.id !== id);
};
Dessa forma a função aceitará qualquer objeto que tiver uma propriedade id
do mesmo tipo que o id
utilizado.
Observações
Agora que você sabe o que é, porque existe e com usar Generics, você pode ter percebido que você já viu ou já utilizou Generics antes sem saber.
O melhor exemplo disso é o array que pode ser utilizado como string[]
, mas esse []
no final de um tipo é apenas um syntax sugar para o tipo genérico de array que é Array<string>
.
Outro fator interessante para saber é que você pode passar explicitamente os parâmetros genéricos utilizando <
+ tipo generico + >
ao chamar uma função ou utilizar um tipo.
Quando não é explicitamente utilizado o parâmetro genérico, o typescript infere o tipo automaticamente pelas variáveis, mas quando é explícito, as variáveis devem seguir estritamente o tipo passado.
const last = <T>(arr: T[]): T => arr[arr.length - 1]
const nArr = [123, 456]
last(nArr)
last<number>(nArr)
last<string>(nArr) // => error
No caso a cima, temos uma função com parâmetro genérico que funciona com o parâmetro não explícito e explícito, mas da erro quando o parâmetro explícito é diferente do da variável utilizada.
Caso você queria assistir em vez de ler, segue o link do video.