back to top

I Generics in TypeScript

TypeScript supporta i tipi generici (Generics) che permettono di definire funzioni, classi o interfacce che sono in grado di lavorare con diversi tipi di dato. Per esempio, è possibile creare una funzione in cui il tipo dei parametri e del valore di ritorno non viene specificato fino al momento in cui la funzione viene invocata.

Detto in altro modo, nel momento in cui la funzione viene definita, i parametri della funzione avranno un tipo generico che chiameremo per esempio T. Solo quando andremo a invocare la funzione indicheremo qual è il tipo di T. In questo modo possiamo definire un’unica funzione che è in grado di lavorare con diversi tipi di dati. Cerchiamo di capire meglio di cosa si tratta attraverso un esempio.

Funzioni generiche

Come abbiamo avuto modo di ripetere più volte, Javascript è un linguaggio a tipizzazione dinamica. Nonostante vi siano degli aspetti negativi, che abbiamo già illustrato in una delle precedenti lezioni, possiamo però creare delle funzioni a cui passare qualsiasi tipo di argomento. Vediamo un semplice esempio in Javascript in cui creiamo una nuova funzione che si limita a stampare il tipo dell’unico argomento passato e restituisce l’argomento stesso come valore di ritorno.

// test.js

function printType(arg) {
  console.log(typeof arg);
  return arg;
}

printType('ciao'); // string

printType(17); // number

printType({a: 1}); // object

Come è possibile notare dal frammento di codice riportato sopra, possiamo passare qualsiasi tipo di argomento alla funzione e otteniamo il risultato atteso. Se proviamo a realizzare un esempio simile usando le annotazioni di tipo di TypeScript, dobbiamo specificare il tipo dell’argomento e del valore di ritorno. Se usiamo quindi il tipo string, non possiamo invocare la funzione con un argomento numerico.

// app.ts

function printType(arg: string): string {
  console.log(typeof arg);
  return arg;
}

printType('ciao'); // string

// ERRORE: Argument of type '17' is not assignable to parameter of type 'string'
printType(17); // number

// ERRORE:  Argument of type '{ a: number; }' is not assignable to parameter of type 'string'
printType({a: 1}); // object

Visualizzeremo infatti dei messaggi di errore nel caso in cui arg non sia di tipo string. Una prima possibile soluzione consiste nel creare una funzione per ogni tipo diverso del parametro arg. Risulta però evidente che non si tratta di una soluzione ottimale. Possiamo allora creare una funzione in cui l’unico argomento in ingresso e il valore di ritorno sono entrambi di tipo any. In questo caso possiamo invocare la funzione con qualsiasi argomento, ma non ha più molto senso usare le annotazioni di tipo visto che perdiamo qualsiasi informazione, specialmente sul valore restituito. È per questo motivo che una migliore soluzione può essere costruita con l’ausilio dei Generics.

function printType<T>(arg: T): T {
  console.log(typeof arg);
  return arg;
}

printType<string>('ciao'); // string

printType<number>(17); // number

/* printType<object>({a: 1})
*
* oppure
*
*/
printType<{a: number}>({a: 1})
printType({a: 1}); // object

Abbiamo definito una funzione generica la quale utilizza un solo tipo parametrico T che viene inserito fra parentesi angolari dopo il nome della funzione stessa. L’unico argomento, che verrà passato alla funzione, sarà di un determinato tipo T che dovrà essere specificato nel momento in cui la funzione viene invocata. Anche il valore di ritorno deve essere dello stesso tipo.

Facendo riferimento al frammento di codice riportato sopra, nel momento in cui invochiamo la funzione, dobbiamo indicare il tipo del parametro T. Nel caso non venga specificato esplicitamente, TypeScript cercherà di inferire il tipo dei parametri e del valore di ritorno.

Come per le funzioni non generiche, possiamo dichiarare il tipo di una funzione generica indicando, in quest’ultimo caso, i parametri generici tra parentesi angolari.

function printType<T>(arg: T[]): T[] {
  console.log(typeof arg);
  return arg;
}

const printFunc: <U>(arg: U[]) => U[] = printType;

printFunc<number>([0, 1, 2]); // object

Il nome dei parametri usati per il tipo della funzione generica non deve necessariamente coincidere con quello dei parametri usati nella definizione della funzione. Nell’esempio riportato in alto abbiamo anche indicato che la funzione printType() aspetta in ingresso un argomento che deve essere un array i cui elementi dovranno essere di un certo tipo ‘T’ da stabilire nel momento in cui la funzione verrà invocata.

Possiamo anche creare un’interfaccia generica, da poter riutilizzare più volte.

interface GenericPrintFn {
  <T>(arg: T[]): T[];
}

function printType<T>(arg: T[]): T[] {
  console.log(typeof arg);
  return arg;
}

const printFunc: GenericPrintFn = printType;

printFunc<number>([0, 1, 2]); // object

È altresì possibile spostare il tipo generico dalla funzione all’intera interfaccia. Questo rende il tipo generico visibile a tutti gli altri membri dell’interfaccia.

interface GenericPrintFn<T> {
  (arg: T): T;
}

function printType<T>(arg: T): T {
  console.log(typeof arg);
  return arg;
}

const printFunc: GenericPrintFn<string> = printType;

printFunc('ciao'); // string

Funzioni generiche con vincolo sul tipo dei parametri

Negli esempi visti finora, abbiamo definito delle funzioni che hanno un parametro il quale può essere di qualsiasi tipo. Vediamo ora un nuovo esempio per capire quali problemi possono sorgere da una scelta simile.

function stampaDettagli<T>(elenco: T[]): void {
  // ERRORE: Property 'dettagli' does not exist on type 'T'.
  elenco.forEach(elemento => {
    elemento.dettagli();
  });
}

Nel frammento di codice riportato sopra, abbiamo definito una funzione stampaDettagli che mostra a video i dettagli di un elenco di elementi. Ci viene mostrato però un messaggio di errore perché il parametro della funzione stampaDettagli() è un array i cui elementi possono essere di un generico tipo ‘T’ il quale potrebbe non avere un metodo dettagli(). Per risolvere questo tipo di errori, possiamo stabilire dei vincoli sul tipo dei parametri. Riscriviamo nuovamente la funzione stampaDettagli() indicando che il parametro elenco deve essere un array i cui elementi possono soltanto essere di un tipo che sia compatibile con l’interfaccia HasDetails.

interface HasDetails {
  dettagli(): string;
}

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

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

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

  dettagli(): string {
    return `Dettagli auto: 
      marca:${this.marca}
      modello: ${this.modello}
      anno: ${this.anno}
    `;
  }
}

class Motocicletta implements Veicolo {
  public marca: string;
  public modello: string;

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

  dettagli(): string {
    return `Dettagli moto -> marca: ${this.marca} - modello: ${this.modello}`;
  }
}

function stampaDettagli<T extends HasDetails>(elenco: T[]): void {
  elenco.forEach(elemento => {
    console.log(elemento.dettagli());
  });
}

const elencoVeicoli = [
  new Automobile('Ferrari', '365 GTB/4', 1972),
  new Automobile('Aston Martin', 'Vantage'),
  new Motocicletta('Yamaha', 'XSR700')
]

stampaDettagli<Veicolo>(elencoVeicoli);

La funzione stampaDettagli() può ora ricevere in ingresso soltanto oggetti che abbiano un metodo dettagli(). Notate che l’interfaccia Veicolo non estende HasDetails. TypeScript infatti controlla che la struttura degli oggetti passati a stampaDettagli() sia compatibile con quella dell’interfaccia HasDetails. Le due classi Automobile e Motocicletta estendono l’interfaccia Veicolo che presenta un metodo dettagli() così come l’interfaccia HasDetails. Un array, contenente delle istanze di Automobile e Motocicletta, può essere quindi passato alla funzione stampaDettagli(). Il risultato dell’esempio sarà simile a quello mostrato sotto.

Dettagli auto:
  marca: Ferrari
  modello: 365 GTB/4
  anno: 1972

Dettagli auto:
  marca: Aston Martin
  modello: Vantage
  anno: n.d.

Dettagli moto -> marca:Yamaha - modello: XSR700

Classi generiche

In maniera simile a quanto abbiamo visto per le funzioni, è possibile anche creare delle classi generiche, come mostrato nell’esempio che segue.

class Pair<T, U> {
  private firstElement: T;
  private secondElement: U;

  constructor(firstElement: T, secondElement: U) {
    this.firstElement = firstElement;
    this.secondElement = secondElement;
  }

  public getFirstElement(): T {
    return this.firstElement;
  }

  public getSecondElement(): U {
    return this.secondElement;
  }

  public getPair(): string {
    return `${this.firstElement} - ${this.secondElement}`;
  }
}

const cars = [
  new Automobile('McLaren', 'P1'),
  new Automobile('Ferrari', 'Portofino'),
  new Automobile('Rolls-Royce', 'Wraith')
];

const firstPair = new Pair<number, string>(3, 'Ayrton Senna');
const secondPair = new Pair<string, Automobile[]>('Lisa', cars);
const thirdPair = new Pair<string, string>('Australia', 'Canberra');
const fourthPair = new Pair<string, string>('ciao', 'hello');

Gli oggetti della classe Pair contengono due elementi che possono essere di qualsiasi tipo. Un istanza della classe Pair può contenere anche due elementi dello stesso tipo. Tali oggetti presentano inoltre tre metodi pubblici. I metodi pair.getFirstElement() e pair.getSecondElement() restituiscono rispettivamente il primo e il secondo elemento contenuti nell’oggetto. Invece pair.getPair() restituisce una stringa contenente il nome di entrambi.

Conclusioni

In questa lezione abbiamo parlato dei Generics che costituiscono una delle funzionalità più utili e interessanti di TypeScript permettendo di creare classi, funzioni e interfacce riutilizzabili con diversi tipi di parametri. Nella prossima lezione parleremo brevemente dei Decorators e vedremo come possono essere utilizzati in differenti contesti.

Pubblicitร 
Articolo precedente
Articolo successivo