Generics Classi

Le classi generiche in TypeScript permettono di creare strutture dati e componenti riutilizzabili che operano su tipi diversi mantenendo type safety completa. Dichiarate con parametri tipo <T>, le classi generiche definiscono placeholder per tipi che vengono specificati all’istanziazione, consentendo di scrivere codice una volta e utilizzarlo con qualsiasi tipo, eliminando duplicazione e garantendo correttezza a compile time.
Fondamenti e Sintassi
Una classe generica si dichiara aggiungendo parametri tipo tra angle brackets dopo il nome della classe.
// Classe generica base
class Box<T> {
constructor(private contenuto: T) {}
getContenuto(): T {
return this.contenuto;
}
setContenuto(valore: T): void {
this.contenuto = valore;
}
}
// Istanziazione con tipo specifico
const boxNumero = new Box<number>(42);
console.log(boxNumero.getContenuto()); // 42
const boxStringa = new Box<string>("hello");
console.log(boxStringa.getContenuto()); // "hello"
// Inferenza del tipo dal costruttore
const boxAuto = new Box(true); // Box<boolean>
Multipli Parametri Tipo
Le classi possono avere multipli parametri tipo per gestire relazioni complesse tra tipi.
// Classe con due parametri tipo
class Coppia<K, V> {
constructor(private chiave: K, private valore: V) {}
getChiave(): K {
return this.chiave;
}
getValore(): V {
return this.valore;
}
setValore(nuovoValore: V): void {
this.valore = nuovoValore;
}
}
const coppia = new Coppia<string, number>("età", 25);
console.log(coppia.getChiave()); // "età"
console.log(coppia.getValore()); // 25
// Mappa generica
class Mappa<K, V> {
private items: Map<K, V> = new Map();
set(chiave: K, valore: V): void {
this.items.set(chiave, valore);
}
get(chiave: K): V | undefined {
return this.items.get(chiave);
}
has(chiave: K): boolean {
return this.items.has(chiave);
}
delete(chiave: K): boolean {
return this.items.delete(chiave);
}
}
const utenti = new Mappa<number, string>();
utenti.set(1, "Mario");
utenti.set(2, "Anna");
Constraints sui Parametri Tipo
Applicare vincoli ai parametri tipo per limitare i tipi accettabili.
// Constraint con extends
class Repository<T extends { id: number }> {
private items: T[] = [];
aggiungi(item: T): void {
this.items.push(item);
}
trovaId(id: number): T | undefined {
return this.items.find((item) => item.id === id);
}
getTutti(): T[] {
return [...this.items];
}
}
interface Utente {
id: number;
nome: string;
}
interface Prodotto {
id: number;
prezzo: number;
}
const repoUtenti = new Repository<Utente>();
repoUtenti.aggiungi({ id: 1, nome: "Mario" });
const repoProdotti = new Repository<Prodotto>();
repoProdotti.aggiungi({ id: 1, prezzo: 99 });
// ERRORE: string non ha proprietà id
// const repoStringhe = new Repository<string>();
Metodi Generici in Classi Generiche
Combinare parametri tipo della classe con parametri tipo dei metodi.
class Collezione<T> {
private items: T[] = [];
aggiungi(item: T): void {
this.items.push(item);
}
// Metodo generico aggiuntivo
mappa<U>(fn: (item: T) => U): U[] {
return this.items.map(fn);
}
// Metodo generico con constraint
filtraETrasforma<U extends T>(predicate: (item: T) => item is U): U[] {
return this.items.filter(predicate);
}
// Metodo che restituisce classe generica diversa
converti<U>(): Collezione<U> {
return new Collezione<U>();
}
}
const numeri = new Collezione<number>();
numeri.aggiungi(1);
numeri.aggiungi(2);
numeri.aggiungi(3);
const stringhe = numeri.mappa((n) => n.toString()); // string[]
const doppi = numeri.mappa((n) => n * 2); // number[]
Proprietà Generiche
Le proprietà di classe possono usare i parametri tipo della classe.
class Wrapper<T> {
// Proprietà del tipo generico
valore: T;
// Array del tipo generico
lista: T[] = [];
// Proprietà opzionale
default?: T;
// Proprietà readonly
readonly immutabile: T;
constructor(valore: T) {
this.valore = valore;
this.immutabile = valore;
}
aggiungiALista(item: T): void {
this.lista.push(item);
}
}
const wrapper = new Wrapper<string>("hello");
wrapper.aggiungiALista("world");
console.log(wrapper.lista); // ["world"]
Ereditarietà con Generics
Le classi generiche possono estendere altre classi generiche o essere estese.
// Classe base generica
class Container<T> {
constructor(protected contenuto: T) {}
getContenuto(): T {
return this.contenuto;
}
}
// Estende classe generica specificando tipo
class StringContainer extends Container<string> {
toUpperCase(): string {
return this.contenuto.toUpperCase();
}
}
// Estende classe generica mantenendo generic
class ValidatedContainer<T> extends Container<T> {
constructor(contenuto: T, private validatore: (item: T) => boolean) {
super(contenuto);
if (!validatore(contenuto)) {
throw new Error("Contenuto non valido");
}
}
setContenuto(nuovoContenuto: T): void {
if (this.validatore(nuovoContenuto)) {
this.contenuto = nuovoContenuto;
}
}
}
const validated = new ValidatedContainer(42, (n) => n > 0);
// Estende aggiungendo parametri tipo
class PairContainer<T, U> extends Container<T> {
constructor(contenuto: T, private secondo: U) {
super(contenuto);
}
getSecondo(): U {
return this.secondo;
}
}
Static Members in Classi Generiche
I membri statici non possono usare i parametri tipo dell’istanza ma possono avere propri generics.
class Factory<T> {
// Proprietà istanza usa T
private item: T;
constructor(item: T) {
this.item = item;
}
// ERRORE: static non può usare T dell'istanza
// static staticItem: T;
// OK: metodo static con proprio generic
static crea<U>(item: U): Factory<U> {
return new Factory(item);
}
// OK: proprietà static non-generic
static contatore: number = 0;
// OK: metodo static con generic indipendente
static confronta<A, B>(a: A, b: B): boolean {
return a === b;
}
}
const factory1 = Factory.crea("hello"); // Factory<string>
const factory2 = Factory.crea(42); // Factory<number>
Default Type Parameters
Specificare tipi default per parametri tipo quando non vengono forniti esplicitamente.
// Parametro tipo con default
class Lista<T = string> {
private items: T[] = [];
aggiungi(item: T): void {
this.items.push(item);
}
getTutti(): T[] {
return [...this.items];
}
}
// Usa default (string)
const listaDefault = new Lista();
listaDefault.aggiungi("hello");
// Specifica tipo esplicitamente
const listaNumeri = new Lista<number>();
listaNumeri.aggiungi(42);
// Multipli default
class Cache<K = string, V = any> {
private storage = new Map<K, V>();
set(key: K, value: V): void {
this.storage.set(key, value);
}
get(key: K): V | undefined {
return this.storage.get(key);
}
}
const cache1 = new Cache(); // Cache<string, any>
const cache2 = new Cache<number>(); // Cache<number, any>
const cache3 = new Cache<string, User>(); // Cache<string, User>
Classi Generiche per Data Structures
Implementare strutture dati comuni con generics.
// Stack generico
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
console.log(stack.pop()); // 2
// Queue generico
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
front(): T | undefined {
return this.items[0];
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
// LinkedList generico
class Node<T> {
constructor(public valore: T, public next: Node<T> | null = null) {}
}
class LinkedList<T> {
private head: Node<T> | null = null;
aggiungi(valore: T): void {
const nuovoNodo = new Node(valore);
if (!this.head) {
this.head = nuovoNodo;
return;
}
let corrente = this.head;
while (corrente.next) {
corrente = corrente.next;
}
corrente.next = nuovoNodo;
}
toArray(): T[] {
const risultato: T[] = [];
let corrente = this.head;
while (corrente) {
risultato.push(corrente.valore);
corrente = corrente.next;
}
return risultato;
}
}
Builder Pattern con Generics
Implementare pattern builder type-safe con classi generiche.
// Builder generico
class QueryBuilder<T> {
private filters: Array<(item: T) => boolean> = [];
private sortFn?: (a: T, b: T) => number;
private limitValue?: number;
where(predicate: (item: T) => boolean): this {
this.filters.push(predicate);
return this;
}
orderBy(compareFn: (a: T, b: T) => number): this {
this.sortFn = compareFn;
return this;
}
limit(n: number): this {
this.limitValue = n;
return this;
}
execute(data: T[]): T[] {
let result = data.filter((item) => this.filters.every((f) => f(item)));
if (this.sortFn) {
result = result.sort(this.sortFn);
}
if (this.limitValue) {
result = result.slice(0, this.limitValue);
}
return result;
}
}
interface Prodotto {
nome: string;
prezzo: number;
categoria: string;
}
const prodotti: Prodotto[] = [
{ nome: "A", prezzo: 100, categoria: "elettronica" },
{ nome: "B", prezzo: 50, categoria: "libri" },
{ nome: "C", prezzo: 150, categoria: "elettronica" },
];
const risultato = new QueryBuilder<Prodotto>()
.where((p) => p.categoria === "elettronica")
.where((p) => p.prezzo > 80)
.orderBy((a, b) => b.prezzo - a.prezzo)
.limit(10)
.execute(prodotti);
Observable/Subject Pattern
Implementare pattern observer con classi generiche.
// Observable generico
class Observable<T> {
private observers: Array<(value: T) => void> = [];
subscribe(observer: (value: T) => void): () => void {
this.observers.push(observer);
// Restituisce funzione unsubscribe
return () => {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
};
}
notify(value: T): void {
this.observers.forEach((observer) => observer(value));
}
}
// Subject con stato
class BehaviorSubject<T> extends Observable<T> {
constructor(private currentValue: T) {
super();
}
getValue(): T {
return this.currentValue;
}
next(value: T): void {
this.currentValue = value;
this.notify(value);
}
subscribe(observer: (value: T) => void): () => void {
// Emette valore corrente immediatamente
observer(this.currentValue);
return super.subscribe(observer);
}
}
const subject = new BehaviorSubject<number>(0);
subject.subscribe((value) => console.log("Observer 1:", value));
subject.next(1); // Observer 1: 1
subject.next(2); // Observer 1: 2
subject.subscribe((value) => console.log("Observer 2:", value)); // Observer 2: 2
subject.next(3); // Observer 1: 3, Observer 2: 3
Singleton Pattern con Generics
Implementare singleton type-safe con classi generiche.
// Singleton base generico
class Singleton<T> {
private static instances = new Map<any, any>();
protected constructor() {}
static getInstance<U>(this: new () => U): U {
if (!Singleton.instances.has(this)) {
Singleton.instances.set(this, new this());
}
return Singleton.instances.get(this);
}
}
class DatabaseConnection extends Singleton<DatabaseConnection> {
private connected = false;
connect(): void {
if (!this.connected) {
console.log("Connessione al database...");
this.connected = true;
}
}
query(sql: string): void {
console.log("Query:", sql);
}
}
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true
Type Guards in Classi Generiche
Implementare type guards per narrowing in classi generiche.
// Classe con type guard
class Result<T, E = Error> {
private constructor(private readonly value?: T, private readonly error?: E) {}
static ok<T, E = Error>(value: T): Result<T, E> {
return new Result<T, E>(value, undefined);
}
static err<T, E = Error>(error: E): Result<T, E> {
return new Result<T, E>(undefined, error);
}
isOk(): this is { value: T } {
return this.value !== undefined;
}
isErr(): this is { error: E } {
return this.error !== undefined;
}
unwrap(): T {
if (this.isOk()) {
return this.value;
}
throw new Error("Called unwrap on Err value");
}
unwrapOr(defaultValue: T): T {
return this.isOk() ? this.value : defaultValue;
}
}
const success = Result.ok<number>(42);
const failure = Result.err<number, string>("errore");
if (success.isOk()) {
console.log(success.unwrap()); // 42
}
if (failure.isErr()) {
console.log("Errore:", failure.unwrapOr(0)); // 0
}
Conclusioni
Le classi generiche in TypeScript forniscono meccanismo potente per creare componenti riutilizzabili e type-safe che operano su tipi parametrici. Combinando parametri tipo, constraints, ereditarietà, e default types, è possibile modellare strutture dati complesse e pattern architetturali mantenendo type safety completa. L’uso di generics in classi elimina duplicazione di codice, migliora manutenibilità, e permette al compilatore di verificare correttezza delle operazioni, risultando essenziale per librerie, framework, e applicazioni che richiedono astrazione su tipi diversi mantenendo garanzie statiche.