Classi

Le classi in TypeScript rappresentano l’implementazione del paradigma di programmazione orientata agli oggetti, fornendo un meccanismo per definire strutture che combinano dati e comportamenti. TypeScript estende il concetto di classe JavaScript aggiungendo tipizzazione statica, modificatori di accesso e controlli compile-time che migliorano significativamente la robustezza e manutenibilità del codice.
Struttura e Sintassi Fondamentale
Una classe TypeScript è un template per creare oggetti che condividono struttura e comportamento comuni. La definizione di classe include proprietà per memorizzare stato e metodi per implementare comportamenti. Il sistema di tipi TypeScript garantisce che le proprietà abbiano tipi consistenti e che i metodi rispettino le loro signature dichiarate.
La keyword class introduce la definizione, seguita dal nome della classe che per convenzione usa PascalCase. All’interno del corpo della classe, le proprietà possono essere dichiarate con tipizzazione esplicita o inferita, mentre i metodi seguono la stessa sintassi delle funzioni ma operano nel contesto dell’istanza della classe.
class Person {
name: string;
age: number;
private id: string;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
this.id = Math.random().toString();
}
greet(): string {
return `Hello, I'm ${this.name}`;
}
celebrateBirthday(): void {
this.age++;
}
}
Costruttori e Inizializzazione
Il costruttore è un metodo speciale che viene invocato automaticamente quando viene creata una nuova istanza della classe. In TypeScript, il costruttore può avere parametri tipizzati e può inizializzare proprietà della classe. Esiste un solo costruttore per classe, ma può essere sovraccaricato attraverso union types nei parametri.
TypeScript offre una sintassi concisa per definire e inizializzare proprietà direttamente nei parametri del costruttore utilizzando modificatori di accesso. Questo pattern riduce il boilerplate eliminando la necessità di dichiarare separatamente le proprietà e assegnarle nel corpo del costruttore.
Il compilatore TypeScript verifica che tutte le proprietà non-optional siano inizializzate nel costruttore o al momento della dichiarazione, prevenendo errori di accesso a proprietà undefined.
Modificatori di Accesso
TypeScript introduce tre modificatori di accesso che controllano la visibilità delle proprietà e metodi: public, private, e protected. Questi modificatori forniscono incapsulamento, uno dei principi fondamentali della programmazione orientata agli oggetti.
Public è il modificatore di default e rende membri accessibili da qualsiasi parte del codice. Private limita l’accesso al solo codice all’interno della stessa classe, mentre protected permette l’accesso alle sottoclassi oltre alla classe stessa.
Questi controlli di accesso sono enforced a compile-time da TypeScript, fornendo un meccanismo per prevenire accessi inappropriati che potrebbero compromettere l’integrità dell’oggetto o violare l’interfaccia pubblica progettata.
Proprietà e Metodi
Le proprietà delle classi possono essere readonly, impedendo modifiche dopo l’inizializzazione. Possono essere optional usando il modificatore ?, e possono avere inizializzatori di default. TypeScript supporta anche proprietà calcolate attraverso getter e setter che permettono di definire logica custom per l’accesso e modifica delle proprietà.
I metodi delle classi sono funzioni che operano nel contesto dell’istanza e hanno accesso alle proprietà attraverso this. Possono essere asincroni, generici, e possono utilizzare overloading per fornire signature multiple. Il binding di this è gestito automaticamente quando i metodi sono chiamati su istanze.
class BankAccount {
readonly accountNumber: string;
private _balance: number = 0;
constructor(accountNumber: string, initialBalance: number = 0) {
this.accountNumber = accountNumber;
this._balance = initialBalance;
}
get balance(): number {
return this._balance;
}
deposit(amount: number): void {
if (amount > 0) {
this._balance += amount;
}
}
withdraw(amount: number): boolean {
if (amount > 0 && amount <= this._balance) {
this._balance -= amount;
return true;
}
return false;
}
}
Ereditarietà e Polimorfismo
L’ereditarietà permette a una classe di estendere un’altra classe, ereditando proprietà e metodi della classe padre. La keyword extends stabilisce la relazione di ereditarietà, mentre super permette di accedere al costruttore e ai metodi della classe padre.
Il polimorfismo consente a istanze di classi derivate di essere trattate come istanze della classe base, permettendo comportamenti specifici attraverso l’override di metodi. TypeScript garantisce che l’override mantenga compatibilità di tipo con il metodo originale.
L’ereditarietà in TypeScript supporta la sostituzione di Liskov, principio che assicura che oggetti di una sottoclasse possano sostituire oggetti della classe padre senza alterare la correttezza del programma.
Classi Generiche
TypeScript supporta classi generiche che possono operare su tipi diversi mantenendo type safety. I parametri di tipo sono specificati dopo il nome della classe e possono essere utilizzati in proprietà, metodi e costruttori.
Le classi generiche sono particolarmente utili per implementare strutture dati riutilizzabili come collezioni, cache e wrapper che devono mantenere informazioni di tipo per il contenuto che gestiscono.
class Container<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
get(index: number): T | undefined {
return this.items[index];
}
getAll(): T[] {
return [...this.items];
}
}
const numberContainer = new Container<number>();
const stringContainer = new Container<string>();
Implementazione di Interfacce
Le classi possono implementare una o più interfacce utilizzando la keyword implements. Questo meccanismo assicura che la classe fornisca implementazioni per tutti i membri definiti nelle interfacce, creando un contratto che la classe deve rispettare.
L’implementazione di interfacce permette polimorfismo basato su interfacce, dove oggetti di classi diverse possono essere trattati uniformemente se implementano la stessa interfaccia. Questo approccio favorisce la composizione over ereditarietà e facilita testing e dependency injection.
Decoratori e Metadati
TypeScript supporta decoratori sperimentali che permettono di annotare e modificare classi, metodi, proprietà e parametri. I decoratori sono funzioni che vengono eseguite a design-time e possono aggiungere funzionalità cross-cutting come logging, validation, dependency injection e reflection.
I decoratori operano attraverso il sistema di metadati di TypeScript, permettendo di ispezionare e modificare la struttura delle classi in modo programmatico. Questo meccanismo è fondamentale per framework come Angular che si basano pesantemente su dependency injection e metadata.
Performance e Memory Management
Le classi TypeScript vengono compilate in funzioni costruttore JavaScript che utilizzano il prototype chain per l’ereditarietà dei metodi. Questo approccio è efficiente in termini di memoria poiché i metodi sono condivisi tra tutte le istanze attraverso il prototype.
Le proprietà di istanza vengono create per ogni oggetto, mentre i metodi rimangono sul prototype. Questa distinzione è importante per comprendere l’impatto sulla memoria di design decisions come l’uso di arrow functions vs metodi normali.
Le classi TypeScript forniscono quindi un framework robusto e type-safe per la programmazione orientata agli oggetti, combinando la flessibilità di JavaScript con le garanzie di sicurezza di un sistema di tipi statici, facilitando la creazione di codice maintainable e scalabile.