Generics Introduzione

I generics in TypeScript permettono di scrivere codice riutilizzabile che opera su tipi diversi mantenendo type safety. Attraverso parametri tipo indicati con <T>, i generics definiscono placeholder per tipi che vengono specificati al momento dell’uso, consentendo di creare funzioni, classi e interfacce flessibili senza sacrificare le garanzie del type system.
Problema e Soluzione
Senza generics, è necessario duplicare codice per tipi diversi o perdere type safety usando any.
// Senza generics: duplicazione
function identitaNumero(valore: number): number {
return valore;
}
function identitaStringa(valore: string): string {
return valore;
}
// Senza generics: perdita type safety
function identitaAny(valore: any): any {
return valore;
}
// Con generics: riutilizzabile e type-safe
function identita<T>(valore: T): T {
return valore;
}
const num = identita(42); // number
const str = identita("hello"); // string
const bool = identita(true); // boolean
Sintassi Base
I generics si dichiarano con angle brackets <T> dove T è il parametro tipo convenzionale.
// Funzione generica
function primo<T>(array: T[]): T | undefined {
return array[0];
}
// Arrow function
const ultimo = <T>(array: T[]): T | undefined => {
return array[array.length - 1];
};
// Classe generica
class Box<T> {
constructor(private contenuto: T) {}
get(): T {
return this.contenuto;
}
}
const boxNum = new Box(42);
const boxStr = new Box("hello");
// Interfaccia generica
interface Coppia<T, U> {
primo: T;
secondo: U;
}
const coppia: Coppia<string, number> = {
primo: "età",
secondo: 30,
};
Inferenza dei Tipi
TypeScript inferisce automaticamente i tipi dai parametri quando possibile.
function coppia<T, U>(a: T, b: U): [T, U] {
return [a, b];
}
// Tipo inferito automaticamente
const risultato = coppia("hello", 42); // [string, number]
// Tipo esplicito
const esplicito = coppia<string, number>("hello", 42);
Multipli Parametri Tipo
È possibile usare multipli parametri tipo per relazioni complesse.
function mappa<T, U>(array: T[], fn: (item: T) => U): U[] {
return array.map(fn);
}
const numeri = [1, 2, 3];
const stringhe = mappa(numeri, (n) => n.toString()); // string[]
Constraints
I constraints limitano i tipi accettabili usando extends.
// Constraint semplice
function lunghezza<T extends { length: number }>(item: T): number {
return item.length;
}
lunghezza("hello"); // OK
lunghezza([1, 2, 3]); // OK
// lunghezza(123); // ERRORE
// Constraint con interface
interface Identificabile {
id: number;
}
function getId<T extends Identificabile>(obj: T): number {
return obj.id;
}
// Constraint con keyof
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const persona = { nome: "Mario", eta: 30 };
const nome = getProp(persona, "nome"); // string
Array e Collezioni
I generics sono fondamentali per array e collezioni type-safe.
// Array generici
const numeri: Array<number> = [1, 2, 3];
const stringhe: string[] = ["a", "b", "c"];
// Metodi array
const doppi = numeri.map((n) => n * 2); // number[]
const pari = numeri.filter((n) => n % 2 === 0); // number[]
// Collezione personalizzata
class Lista<T> {
private items: T[] = [];
aggiungi(item: T): void {
this.items.push(item);
}
get(index: number): T | undefined {
return this.items[index];
}
}
Vantaggi dei Generics
I generics offrono benefici chiave rispetto ad alternative.
// Riutilizzo del codice
function wrapArray<T>(value: T): T[] {
return [value];
}
wrapArray(1); // number[]
wrapArray("hello"); // string[]
wrapArray(true); // boolean[]
// Type safety preservata
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const merged = merge({ a: 1 }, { b: "hello" });
// { a: number } & { b: string }
// Documentazione implicita
function filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
return array.filter(predicate);
}
Casi d’Uso Comuni
I generics risolvono pattern ricorrenti nella programmazione.
// Repository pattern
interface Repository<T> {
find(id: number): T | undefined;
save(item: T): void;
delete(id: number): void;
}
// Response wrapper
interface Response<T> {
success: boolean;
data: T;
error?: string;
}
// Promise e async
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
// Utility functions
function clone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
Nomenclatura Convenzionale
Convenzioni comuni per parametri tipo migliorano leggibilità.
// T: Type (generico)
function identity<T>(value: T): T {
return value;
}
// K, V: Key, Value
interface Map<K, V> {
get(key: K): V | undefined;
set(key: K, value: V): void;
}
// E: Element
class Stack<E> {
private items: E[] = [];
push(item: E): void {
this.items.push(item);
}
}
// R: Return type
interface Transformer<T, R> {
transform(input: T): R;
}
Quando Usare Generics
I generics sono appropriati per codice che opera su tipi multipli mantenendo relazioni di tipo.
// USARE generics: tipo di input determina output
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// NON usare generics: input e output indipendenti
function getRandomNumber(): number {
return Math.random();
}
// USARE generics: struttura dati riutilizzabile
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
}
Conclusioni
I generics in TypeScript forniscono meccanismo essenziale per scrivere codice riutilizzabile e type-safe. Eliminando necessità di duplicazione o uso di any, i generics permettono di creare funzioni, classi e interfacce che operano su tipi diversi mantenendo garanzie complete del type system. Comprendere quando e come usare generics risulta fondamentale per sfruttare appieno la potenza di TypeScript, abilitando astrazione elegante senza sacrificare sicurezza dei tipi.