I Generics permettono di creare funzioni, classi e altri componenti riutilizzabili che possono lavorare con tipi diversi, mantenendo allo stesso tempo i controlli di tipo di TypeScript.
Senza i Generics, per rendere una funzione utilizzabile con valori di tipo diverso potremmo essere tentati di utilizzare any. Questo però elimina gran parte dei vantaggi della tipizzazione statica.
I Generics permettono di specificare che il tipo verrà definito in modo preciso al momento dell'utilizzo del componente.
Prima di vedere come definire tipi generici, vediamo perché in alcuni casi è necessario utilizzarli.
Supponiamo di voler creare una funzione che restituisca il valore ricevuto:
function restituisci(valore: any): any {
return valore;
}Questa funzione può ricevere valori di qualsiasi tipo:
const numero = restituisci(10);
const testo = restituisci("Ciao");Il problema è che any disattiva i controlli di tipo, essendo un tipo di valore sconosciuto. TypeScript non riesce quindi a mantenere una relazione precisa tra il valore passato alla funzione e quello restituito.
Possiamo ottenere lo stesso risultato utilizzando un Generic:
function restituisci<T>(valore: T): T {
return valore;
}In questo caso T rappresenta un tipo generico che verrà determinato quando la funzione viene utilizzata.
Se passiamo un numero, TypeScript dedurrà che T è un number:
const numero = restituisci(10);Allo stesso modo, se passiamo una stringa, TypeScript dedurrà che T è una string.
La lettera T è comunemente utilizzata per indicare una Type Variable, cioè una variabile che rappresenta un tipo.
Ad esempio:
function restituisci<T>(valore: T): T {
return valore;
}La parte <T> dichiara il parametro di tipo T. Il parametro viene poi utilizzato sia per il parametro della funzione valore: T sia per il suo valore di ritorno : T.
In questo modo TypeScript mantiene il collegamento tra il tipo del valore ricevuto e quello del valore restituito.
Nella maggior parte dei casi non è necessario specificare manualmente il tipo generico. Ad esempio:
function primoElemento<T>(array: T[]): T {
return array[0];
}Possiamo chiamare la funzione passando un array di numeri:
const numero = primoElemento([10, 20, 30]);TypeScript deduce automaticamente T = number e, di conseguenza, numero viene considerato un number.
È però possibile indicare manualmente il parametro generico. Per capire come farlo, utilizzando la funzione:
function restituisci<T>(valore: T): T {
return valore;
}Possiamo specificare T direttamente:
const numero = restituisci<number>(10);In questo caso stiamo dicendo esplicitamente a TypeScript che T = number.
Uno dei principali vantaggi dei Generics è che permettono di mantenere una relazione tra diversi elementi di una funzione.
Consideriamo, ad esempio:
function creaCoppia<T>(valore: T): [T, T] {
return [valore, valore];
}Passiamo ora un numero alla funzione:
const coppia = creaCoppia(10);Nell'esempio, TypeScript sa che il risultato è [number, number].
Il tipo T permette quindi di mantenere la relazione tra il parametro ricevuto e il risultato prodotto.
Una funzione può avere più parametri di tipo. Ad esempio:
function creaCoppia<T, U>(primo: T, secondo: U): [T, U] {
return [primo, secondo];
}In questo caso abbiamo due type variable <T, U>: T rappresenta il tipo del primo parametro, mentre U rappresenta quello del secondo.
Possiamo utilizzarla così:
const coppia = creaCoppia("Andrea", 30);TypeScript deduce automaticamente che T è di tipo string e U è di tipo number. Di conseguenza, coppia viene considerata [string, number].