Hai una funzione che funziona con array di numeri, poi ne serve una identica per stringhe, poi per oggetti. Scrivi tre versioni quasi uguali. Il codice si gonfia, si duplica, e quando devi modificare la logica devi ricordarti di aggiornarle tutte. È un problema concreto che incontriamo spesso nei progetti Laravel e Vue che seguiamo.
Noi, di Meteora Web, lo vediamo nei refactoring: funzioni che prendono any e perdono tutta la sicurezza del tipo, oppure overload manuali che diventano un incubo da mantenere. I Generics risolvono esattamente questo: scrivi la logica una volta sola, lasciando al chiamante la libertà di decidere il tipo, ma con la garanzia che TypeScript controllerà tutto.
In questa guida vediamo cosa sono, come usarli, e come evitare gli errori più comuni. Partiamo dal problema, non dalla teoria.
Cosa sono i Generics in TypeScript e perché servono per codice riutilizzabile?
Un generic è un parametro di tipo. Invece di fissare un tipo (es. number), scrivi <T> e usi T come segnaposto. Quando chiami la funzione, TypeScript deduce il tipo reale da ciò che passi.
Esempio base senza generics:
function firstElement(arr: number[]): number | undefined {
return arr[0];
}
// Se vuoi per stringhe, la riscrivi:
function firstElementStr(arr: string[]): string | undefined { ... }
Con generics:
Sponsored Protocol
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = firstElement([1,2,3]); // type number | undefined
const str = firstElement(['a','b']); // type string | undefined
Il tipo viene inferito automaticamente. Scriviamo una funzione, non tre. Questo è il cuore della riutilizzabilità senza sacrificare la type safety.
Errori comuni da evitare
Il più frequente? Usare any al posto di un generic. any disabilita i controlli; un generic li mantiene. Un altro errore è dimenticare il tipo restituito: se la funzione restituisce qualcosa di diverso da T, TypeScript te lo segnala.
Come si scrive e utilizza una funzione generica in TypeScript?
La sintassi è semplice: dopo il nome della funzione, tra <>, dichiari uno o più parametri di tipo. Puoi usarli nei parametri, nel tipo di ritorno e nel corpo.
function identity<T>(arg: T): T {
return arg;
}
// Chiamata esplicita
const result = identity<number>(42);
// Oppure inferita (più comune)
const result2 = identity('ciao');
Puoi avere più generics:
Sponsored Protocol
function merge<A, B>(obj1: A, obj2: B): A & B {
return { ...obj1, ...obj2 };
}
const merged = merge({ name: 'Alice' }, { age: 30 });
// type: { name: string; } & { age: number; }
Operatività immediata: Apri il tuo progetto TypeScript, trova una funzione che usa any e sostituiscila con un generic. Prova a passare tipi diversi e vedi se TypeScript ti dà errori incoerenti.
Come vincolare i Generics con constraint per maggiore sicurezza?
A volte non vuoi accettare qualsiasi tipo, ma solo quelli che hanno certe proprietà. Usi extends per vincolare il generic.
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength('hello'); // ok, string ha length
getLength([1,2,3]); // ok
getLength(42); // errore: number non ha length
Un caso più utile: estrarre una proprietà da un oggetto in modo type-safe.
function getProperty<Obj, Key extends keyof Obj>(obj: Obj, key: Key): Obj[Key] {
return obj[key];
}
const person = { name: 'Mario', age: 35 };
const name = getProperty(person, 'name'); // type string
const age = getProperty(person, 'age'); // type number
// getProperty(person, 'surname'); // errore: 'surname' non in keyof
Questo è un pattern che usiamo quotidianamente nei gestori di stato e nelle API di validazione.
Sponsored Protocol
Condivisione di vincoli tra più generics
function copyFields<T, U extends T>(source: T, target: U): U {
Object.assign(target, source);
return target;
}
Garantisce che U abbia almeno le stesse proprietà di T, senza perderne di aggiuntive.
Quando usare tipi generici avanzati: infer, conditional types, mapped types?
I generics non si limitano alle funzioni. Puoi usarli per creare tipi condizionali che cambiano in base al tipo passato.
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<'hello'>; // 'yes'
type B = IsString<42>; // 'no'
Il costrutto infer permette di estrarre il tipo da un altro tipo complesso:
type ArrayItem<T> = T extends (infer U)[] ? U : never;
type Item = ArrayItem<number[]>; // number
I mapped types usano generics per trasformare le proprietà di un oggetto:
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Point = { x: number; y: number };
type ReadonlyPoint = Readonly<Point>; // { readonly x: number; readonly y: number }
In pratica, li usi quando devi creare utility type su misura. Per esempio, per rendere tutte le proprietà opzionali o obbligatorie in una DTO.
Sponsored Protocol
Operatività: Nel tuo editor, prova a definire un mapped type che aggiunga un prefisso a tutte le chiavi di un oggetto. Usa keyof e template literal types.
Quale errore comune si fa con i Generics e come evitarlo?
L'errore più subdolo: pensare che il generic sia valutato a runtime. Non lo è. I generics sono solo un costrutto a tempo di compilazione. Non puoi fare typeof T o instanceof T nel codice runtime. TypeScript li cancella dopo la compilazione.
function createInstance<T>(ctor: new () => T): T {
return new ctor();
}
// Questo funziona perché passi il costruttore a runtime, non T.
// Sbagliato:
function logType<T>() {
console.log(typeof T); // errore: T è solo un tipo
}
Un altro errore è non specificare il generic quando TypeScript non riesce a inferirlo automaticamente. In quei casi, fornisci il tipo esplicitamente.
function wrapInArray<T>(item: T): T[] {
return [item];
}
const arr = wrapInArray(5); // ok
const arr2 = wrapInArray<string>(5); // errore: 5 non è stringa
Ricorda: il generic non è un any con un altro nome. Se il tuo codice usa il generic solo per passarlo senza controlli, stai annullando il vantaggio.
Sponsored Protocol
Cosa fare adesso
Ecco tre azioni immediate per applicare quello che hai imparato:
- Trova nel tuo progetto una funzione che ha overload per due o tre tipi diversi (es.
fetchData(url: string): Promise<User> | Promise<Product>). Convertila in un unica funzione generica. - Aggiungi un vincolo
extendsa una funzione che accetta oggetti conid: number, in modo che il chiamante non possa passare un oggetto senza quella proprietà. - Leggi la sezione ufficiale sui Generics nella TypeScript Handbook per approfondire gli use case avanzati.
I generics sono uno strumento potente: ti permettono di scrivere codice flessibile senza rinunciare alla rigidità del tipo. Noi li usiamo in ogni progetto, dal backend Laravel con TypeScript fino ai componenti Vue. Se vuoi vedere come li applichiamo in contesti reali, dai un'occhiata alla nostra guida completa su TypeScript.