back to top

Le classi in TypeScript

La programmazione ad oggetti in JavaScript si basa sul modello prototipale che si differenzia da quello di altri linguaggi come Java o PHP. In Javascript non esiste il concetto di classe. Vengono sfruttate le funzioni (Function constructor) per simulare il modello di programmazione a oggetti classico. Javascript supporta il meccanismo dell’ereditarietà attraverso l’uso degli oggetti prototype. Ogni oggetto ha al suo interno una proprietà prototype che punta a un altro oggetto il quale contiene a sua volta una proprietà prototype e così via fino a raggiungere un oggetto base. In questo modo si viene a creare quella che prende il nome di Prototype Chain (catena di prototipi).

Per rendere la programmazione ad oggetti più simile ad altri linguaggi, a partire da ES2015 (ECMAScript 2015) è stato introdotto il costrutto class che costituisce comunque solo una semplificazione dal punto di vista sintattico. Dietro le quinte il modello di ereditarietà rimane sempre quello prototipale che si basa appunto sull’oggetto Prototype e sulla catena dei prototipi.

Come creare una classe in TypeScript

TypeScript supporta le funzionalità tipiche dei linguaggi di programmazione orientati agli oggetti come le classi e le interfacce. TypeScript usa la sintassi introdotta in ES2015 per la definizione di una classe, ma la estende aggiungendo delle funzionalità ancora non presenti nella versione corrente di Javascript.

Possiamo definire una semplice classe Car come mostrato nel seguente esempio.

class Car {
  modello: string;
  marca: string;

  constructor(modello: string, marca: string) {
    this.modello = modello;
    this.marca = marca;
  }

  dettagliAuto():string {
    return `${this.marca} - ${this.modello}`;
  }
}

Abbiamo definito una classe Car che ha due proprietà di tipo string, modello e marca, che sono pubbliche, dato che non abbiamo specificato nessun altro modificatore di accesso. All’interno del costruttore ci limitiamo a inizializzare le due proprietà con i valori passati come argomenti. Abbiamo inoltre aggiunto un metodo pubblico dettagliAuto() che mostra le informazioni sul modello di auto. La funzione constructor è il costruttore della classe e viene invocata nel momento in cui viene creata una nuova istanza, come vedremo a breve. Generando il codice ES5 corrispondente, otterremo il risultato mostrato sotto.

var Car = (function () {
    function Car(modello, marca) {
        this.modello = modello;
        this.marca = marca;
    }
    Car.prototype.dettagliAuto = function () {
        return this.marca + " - " + this.modello;
    };
    return Car;
}());

Come possiamo notare, il metodo dettagliAuto() diventa una proprietà dell’oggetto Car.prototype. In questo modo viene condiviso fra le varie istanze della classe Car, permettendo un migliore utilizzo della memoria. Qualora si voglia creare comunque una funzione che sia direttamente parte dell’oggetto e non del suo prototype, è possibile usare la sintassi mostata sotto.

class Car {
  modello: string;
  marca: string;

  dettagliAuto: () => string;

  constructor(modello: string, marca: string) {
    this.modello = modello;
    this.marca = marca;

    this.dettagliAuto = () => `${this.marca} - ${this.modello}`;
  }
}

Lanciando come al solito il comando tsc, otteniamo il risultato riportato in basso.

var Car = (function () {
    function Car(modello, marca) {
        var _this = this;
        this.modello = modello;
        this.marca = marca;
        this.dettagliAuto = function () { return _this.marca + " - " + _this.modello; };
    }
    return Car;
}());

Come creare un’istanza di una classe in TypeScript

Per creare una nuova istanza di una classe, per esempio un nuovo oggetto di tipo Car, useremo l’operatore new.

const bmw = new Car('507', 'BMW');

// bmw è un oggetto di tipo Car
console.log(bmw instanceof Car); // true

Ereditarietà delle classi

TypeScript supporta l’ereditarietà singola per le classi. Per estendere una classe base useremo la parola chiave extends quando definiamo la classe derivata. Nel costruttore di quest’ultima dovremo eseguire super() prima di ogni altra operazione per invocare il costruttore della classe base.

class Vehicle {
  modello: string;
  marca: string;
  tipoVeicolo: string;

  constructor(marca: string, modello: string, tipoVeicolo: string) {
    this.marca = marca;
    this.modello = modello;
    this.tipoVeicolo = tipoVeicolo;
  }

  dettagliVeicolo(): string {
    return `${this.tipoVeicolo} - ${this.marca} - ${this.modello}`;
  }
}

class Car extends Vehicle {
  constructor(marca: string, modello: string) {
    super(marca, modello, 'auto');
  }
}

class Motorbike extends Vehicle {
  constructor(marca: string, modello: string) {
    super(marca, modello, 'moto');
  }
}

const jaguar = new Car('Jaguar', 'F-Type');
const ducati = new Motorbike('Ducati', 'Panigale V4');

console.log(jaguar instanceof Car); // true
console.log(ducati instanceof Motorbike) // true
console.log(ducati instanceof Car) // false

Possiamo ridefinire i metodi della classe base e invocare questi ultimi usando super.nomeMetodoClasseBase().

class Vehicle {
  modello: string;
  marca: string;
  tipoVeicolo: string;

  constructor(marca: string, modello: string, tipoVeicolo: string) {
    this.marca = marca;
    this.modello = modello;
    this.tipoVeicolo = tipoVeicolo;
  }

  dettagliVeicolo(): string {
    return `${this.tipoVeicolo} - ${this.marca} - ${this.modello}`;
  }
}

class Car extends Vehicle {
  constructor(marca: string, modello: string) {
    super(marca, modello, 'auto');
  }
  dettagliVeicolo(): string {
    console.log('Dettagli auto:');
    return super.dettagliVeicolo();
  }
}

È possibile invece eseguire l’overloading dei metodi nello stesso modo in cui abbiamo eseguito l’overloading della funzione somma() alla fine della precedente lezione.

Modificatori di accesso: public, private, protected, readonly

Le proprietà definite all’interno di una classe sono pubbliche, se non specificato diversamente con l’ausilio dei cosiddetti modificatori di accesso.

L’esempio visto in precedenza può essere riscritto in maniera equivalente usando esplicitamente la keyword public.

Le proprietà public sono accessibili e modificabili all’esterno della classe.

class Car {
  public modello: string;
  public marca: string;

  constructor(marca: string, modello: string) {
    this.modello = modello;
    this.marca = marca;
  }

  dettagliAuto() {
    return `${this.marca} - ${this.modello}`;
  }
}

const jaguar = new Car('Jaguar', 'F-Type');
jaguar.modello += ' SVR';

console.log(jaguar.modello); // F-Type SVR

Le proprietà private non sono invece accessibili all’esterno della classe.

class Car {
  public modello: string;
  private marca: string;

  constructor(marca: string, modello: string) {
    this.modello = modello;
    this.marca = marca;
  }

  dettagliAuto() {
    return `${this.marca} - ${this.modello}`;
  }
}

const jaguar = new Car('Jaguar', 'F-Type');
jaguar.modello += ' SVR';

// ERRORE!
// Property 'marca' is private and only accessible within class 'Car'.
console.log(jaguar.marca);  

// ERRORE
// Proprietà privata e non accessibile all'esterno della classe 'Car'.
jaguar.marca = 'Volvo';

Le proprietà o i metodi private di una classe base non sono accessibili neanche dalla classe derivata.

class Vehicle {
  private modello: string;
  private marca: string;
  private tipoVeicolo: string;

  constructor(marca: string, modello: string, tipoVeicolo: string) {
    this.marca = marca;
    this.modello = modello;
    this.tipoVeicolo = tipoVeicolo;
  }

  dettagliVeicolo(): string {
    return `${this.tipoVeicolo} - ${this.marca} - ${this.modello}`;
  }
}

class Car extends Vehicle {
  constructor(marca: string, modello: string) {
    super(marca, modello, 'auto');
  }
  public dettagliVeicolo(): string {
    console.log('Dettagli auto:');
    return super.dettagliVeicolo();
  }
  public modelloAuto() {
    // ERRORE
    // [ts] Property 'modello' is private and only accessible within class 'Vehicle'.
    return this.modello;
  }
}

const jaguar = new Car('Jaguar', 'F-Type');

console.log(jaguar.dettagliVeicolo());

In questi casi è possibile usare il modificatore protected che è simile a private, ma le classi derivate hanno accesso a questo tipo di metodi e proprietà.

class Vehicle {
  protected modello: string;
  protected marca: string;
  protected tipoVeicolo: string;

  constructor(marca: string, modello: string, tipoVeicolo: string) {
    this.marca = marca;
    this.modello = modello;
    this.tipoVeicolo = tipoVeicolo;
  }

  dettagliVeicolo(): string {
    return `${this.tipoVeicolo} - ${this.marca} - ${this.modello}`;
  }
}

class Car extends Vehicle {
  constructor(marca: string, modello: string) {
    super(marca, modello, 'auto');
  }
  public dettagliVeicolo(): string {
    console.log('Dettagli auto:');
    return super.dettagliVeicolo();
  }
  public modelloAuto() {
    return this.modello; // OK, modello è protected
  }
}

const jaguar = new Car('Jaguar', 'F-Type');

console.log(jaguar.dettagliVeicolo());

In TypeScript è possibile definire e assegnare un valore a una proprietà direttamente nel costruttore. Per far ciò basta usare la sintassi riportata sotto.

class Car {
  // Definiamo delle proprietà direttamente nel costruttore.
  // Saranno inizializzate coi valori passati nel momento in cui viene istanziata
  // un nuovo oggetto di tipo Car.
  constructor(private marca: string, private modello: string) {
    console.log('Costruttore classe Car');
  }

  dettagliAuto() {
    return `${this.marca} - ${this.modello}`;
  }
}

const jaguar = new Car('Jaguar', 'F-Type'); // Costruttore classe Car
console.log(jaguar.dettagliAuto()); // Jaguar - F-Type

Come è possibile notare, abbiamo dichiarato le due proprietà marca e modello che nello stesso tempo vanno considerati come parametri del costruttore. Quando creiamo la nuova istanza jaguar, i valori ‘Jaguar’ e ‘F-Type’ verranno automaticamente assegnati alle proprietà private marca e modello dell’oggetto. Sebbene sia disponibile questo tipo di sintassi, il codice risulta poco leggibile. È bene sapere che esiste questa possibilità nel caso si dovessero incontrare frammenti di codice che ne fanno uso, ma è comunque meglio utilizzare la sintassi vista negli esempi precedenti che è sicuramente più chiara.

Get e Set per intercettare l’accesso alle proprietà di un oggetto

Per spiegare i getter e setter partiamo da un semplice esempio.

class Color {
  private _rgb: number[];
  private _hex: string;

  get hex(): string {
    return this._hex;
  }

  set hex(hexColor: string) {
    const color = hexColor.startsWith('#') 
      ? hexColor.substring(1)
      : hexColor;
    this._rgb = color.match(/.{2}/g).map(chunk => parseInt(chunk, 16));
    this._hex = `#${color.toUpperCase()}`;
  }

  printRGB() {
    console.log(this._rgb);
  }
}

const color = new Color();
color.hex = '4990e2';

console.log(color.hex); // #4990E2

color.printRGB(); // [ 73, 144, 226 ]

Supponiamo di avere una classe Color all’interno della quale sono presenti due proprietà private _rgb e _hex. Abbiamo definito due metodi particolari hex() preceduti dalle keyword get e set. Ogni istanza della classe Color avrà una proprietà pubblica hex (senza il carattere _ davanti a ‘hex’) accessibile in lettura e scrittura. Facendo riferimento all’esempio, quando stampiamo a video il contenuto di color.hex (console.log(color.hex)), viene semplicemente restituito il valore della proprietà privata this._hex la quale contiene il valore che gli viene assegnato nel setter nel momento in cui settiamo il valore della proprietà pubblica color.hex (color.hex = '4990e2'). In questo caso prendiamo il valore ricevuto e rimuoviamo l’eventuale carattere ‘#’ iniziale. La stringa ottenuta (‘4990e2’ nell’esempio) viene suddivisa in gruppi di due caratteri. Sull’array ottenuto viene invocata la funzione Array.prototype.map() che ad ogni iterazione trasforma la stringa di due caratteri esadecimali in valore decimale. L’array ottenuto contiene i tre valori RGB.

Proprietà e metodi statici

In TypeScript è possibile definire proprietà o metodi statici, ovvero membri di una classe condivisi da tutte le istanze della classe stessa e accessibili direttamente dalla classe senza la necessità di dover prima creare un’istanza. Visto che non è necessario creare un’istanza della classe per usare proprietà e metodi statici, è immediato intuire che all’interno di un metodo statico si può accedere solo a proprietà e metodi statici. Al contrario in un metodo di istanza è sempre possibile accedere a variabili e metodi statici.

class Helper {
  static getCurrentDate(separator?: string): string {
    separator = separator || '/';
    const currentDate = new Date();
    const day = currentDate.getDate();
    const month = currentDate.getMonth() + 1;
    const year = currentDate.getFullYear();
    return `${day}${separator}${month}${separator}${year}`;
  }
}

console.log(Helper.getCurrentDate('-')); // 23-02-2018

Classi astratte

TypeScript supporta le classi astratte (identificate dalla keyword abstract), ovvero classi base che non possono essere istanziate e che sono caratterizzate da almeno un metodo che viene solo dichiarato senza essere però implementato. Tali metodi vengono identificati dalla keyword abstract e dovranno essere implementati dalle classi che estendono la classe astratta. Una classe che presenta almeno un metodo astratto viene considerata una classe astratta che può però contenere, al contrario delle interfacce, dei metodi che vengono implementati nella classe astratta stessa.

abstract class Veicolo {
  private colore: string;
  protected modello: string;
  protected marca: string;

  constructor(marca: string, modello: string, colore: string) {
      this.marca = marca;
      this.modello = modello;
      this.colore = colore;
  }

  public vernicia(colore: string): void {
    this.colore = colore;
  }
  public coloreCorrente(): string {
    return this.colore;
  }
  public abstract dettagliVeicolo(): string;
}

class Automobile extends Veicolo {
  constructor(marca: string, modello: string, colore: string) {
    super(marca, modello, colore);
  }
  dettagliVeicolo(): string {
    return `${this.marca} - ${this.modello}`;
  }
}

// ERRORE!
// Cannot create an instance of an abstract class
const veicolo = new Veicolo('Jaguar', 'F-Type', 'nero'); 

const jaguar = new Automobile('Jaguar', 'F-Type', 'nero');
console.log(jaguar.dettagliVeicolo()) // Jaguar - F-Type
console.log(jaguar.coloreCorrente()) // nero
jaguar.vernicia('rosso');
console.log(jaguar.coloreCorrente()); // rosso

L’esempio mostra il caso di una classe astratta Veicolo che ha un metodo astratto dettagliVeicolo() il quale deve essere implementato dalla classe Automobile che estende Veicolo. I metodi vernicia() e coloreCorrente() sono invece implementati nella classe base. Nonostante abbiamo definito un costruttore in Veicolo, che viene invocato nel costruttore delle classi che estendono Veicolo, non è possibile creare un’istanza della classe Veicolo. Abbiamo dovuto definire un costruttore nella classe base perché è presente un costruttore nelle classi derivate in cui bisogna per forza invocare super(marca, modello, colore) per inizializzare la classe base.

Classi e Interfacce

Abbiamo già incontrato le interfacce quando abbiamo parlato dei tipi in TypeScript. Un interfaccia definisce un contratto che deve essere rispettato da tutte le classi che la implementano. Per indicare che una classe implementa un’interfaccia usiamo la keyword implements al posto di extends.

interface Veicolo {
  modello: string;
  marca: string;
  dettagliVeicolo(): string;
}

class Automobile implements Veicolo {
  public marca: string;
  public modello: string;
  public anno: number

  constructor(marca: string, modello: string, anno: number) {
    this.marca = marca;
    this.modello = modello;
    this.anno = anno;
  }

  dettagliVeicolo(): string {
    return `${this.marca} - ${this.modello} - ${this.anno}`;
  }
}

Al contrario degli oggetti semplici, che avevamo visto nelle precedenti lezioni, quando una classe estende un’interfaccia, può avere delle proprietà che non sono presenti nell’interfaccia. In questo caso l’interfaccia definisce il numero minimo e il tipo di proprietà e metodi che una classe deve avere per poter implementare l’interfaccia correttamente.

Una classe non può estendere più di una classe, ma può implementare più interfacce.

interface Veicolo {
  modello: string;
  marca: string;
  dettagliVeicolo(): string;
}

interface MezzoDiTrasporto {
  inquinante: boolean;
  tipoMezzoDiTrasporto: string;
}

class Automobile implements Veicolo, MezzoDiTrasporto {
  public marca: string;
  public modello: string;
  public anno: number | string;
  public inquinante: boolean;
  public tipoMezzoDiTrasporto: string;

  constructor(marca: string, modello: string, anno?: number) {
    this.marca = marca;
    this.modello = modello;
    this.anno = anno || 'n.d.';
    this.inquinante = true;
    this.tipoMezzoDiTrasporto = 'veicolo terrestre';
  }

  dettagliVeicolo(): string {
    return `${this.marca} - ${this.modello} - ${this.anno}`;
  }
}

Conclusioni

In questa lezione abbiamo realizzato una panoramica sulla programmazione ad oggetti in TypeScript e abbiamo visto come creare delle nuove classi, come aggiungere delle proprietà e dei metodi e come cambiare la visibilità di questi ultimi. Abbiamo inoltre illustrato in che modo è possibile aggiungere dei membri statici a una classe. Abbiamo infine visto come estendere delle altre classi o implementare delle interfacce. Nella prossima lezione parleremo dei cosiddetti Generics.

Pubblicitร 
Articolo precedente
Articolo successivo