Parte III — Il front-end JavaScript · Capitolo 10

TypeScript, il superset tipizzato

~40 min di lettura3 widget interattivi2 tavole

In questo capitolo

  1. Le problematiche di JavaScript
  2. Perché JavaScript resta indispensabile
  3. L'idea: linguaggi che compilano in JavaScript
  4. TypeScript: definizione e linguaggi super-set
  5. Tipizzazione statica: i tipi primitivi
  6. Array, Enum, Any e type inference
  7. Funzioni
  8. Classi
  9. Interfacce e moduli
  10. Compilazione: tsc, tsconfig e il caso Greeter
  11. Integrazione con JavaScript e Typings
  12. Verifica le tue conoscenze

1. Le problematiche di JavaScript

Il capitolo si apre con un elenco senza mezzi termini delle problematiche di JavaScript: nessuna compilazione, tipizzazione "dinamica", variabili globali, l'Abstract Equality Comparison Algorithm (le regole, spesso sorprendenti, con cui == confronta valori di tipo diverso), scope inconsistencies, modello prototype based e difficile organizzazione architetturale per applicazioni grandi. Il "…" finale lascia intendere che l'elenco potrebbe continuare.

JavaScript ha però anche aspetti positivi, e le slide li dichiarano subito: setup immediato, ecosistema enorme e, soprattutto, la computazione lato client su browser — l'unica tecnologia che i browser eseguono nativamente.

2. Perché JavaScript resta indispensabile

I browser sono stati progettati per supportare JavaScript, per lo scripting lato client, e nel tempo è diventato lo standard indiscusso per la programmazione basata su browser: ogni browser è dotato di un motore runtime JavaScript, basato su ECMA Script. Le slide annotano con ironia che non si è mai arrivati — ci arriveremo mai? — al punto in cui si potrebbe lasciar perdere JavaScript per passare a qualcos'altro, migliore o meglio progettato. Nel tempo JavaScript si è affermato anche nello sviluppo di app per device mobili e per la computazione lato server: è il paradigma Javascript EveryWhere — lo stesso stack MEVN/MEAN che stiamo studiando ne è una dimostrazione.

3. L'idea: linguaggi che compilano in JavaScript

Per superare difetti e limitazioni di JavaScript si sono diffusi molti linguaggi di programmazione alternativi che compilano in JavaScript. La visione che li accomuna è sintetizzata in una frase: «JavaScript is assembly language for the web». Gli esempi citati sono CoffeeScript, TypeScript, ClojureScript, Haxe, Scala e Dart (di Google). Tutti risolvono lo stesso problema in modi diversi: dare allo sviluppatore un linguaggio più espressivo, che poi diventa JavaScript.

4. TypeScript: definizione e linguaggi super-set

TypeScript nasce in Microsoft, e le slide riportano la definizione del suo co-fondatore Anders Hejlsberg:

Definizione

«The goal was to strengthen JavaScript with things like classes, modules, and static typing, without sacrificing the advantage of its being open-standards and cross-platform; the result was a language for application scale JavaScript development, built as a super set of the language». In una riga: TypeScript è un typed superset of JavaScript che compila in plain JavaScript — "Any browser. Any host. Any OS. Open source".

Lo scopo principale dei linguaggi nati come super-set di altri linguaggi è aggiungere feature e migliorare la potenza espressiva. Questo è possibile grazie alla compilazione di codice sorgente, scritto nel linguaggio super-set, in altro codice sorgente scritto nel linguaggio di partenza: si tratta di una compilazione source-to-source, chiamata transpiling, per distinguerla dalla compilazione classica source-to-binary. Il linguaggio super-set può avere caratteristiche semantiche o sintattiche diverse dal linguaggio originale, e il transpiler (il compilatore che esegue il transpiling) ha il duplice compito di controllo degli errori e di generazione del nuovo codice sorgente.

5. Tipizzazione statica: i tipi primitivi

La funzionalità più nota di TypeScript è l'annotazione del tipo opzionale: si può definire il tipo di una variabile — var age: number = 40; — ma è una funzionalità puramente a design time: nessun codice aggiuntivo viene inserito nel JavaScript prodotto dal compilatore, che emette semplicemente var age = 40;. Il vantaggio si vede quando il tipo è violato: var age: number = 'forty'; produce Error: Cannot convert 'string' to 'number'.

I tipi statici si dividono in primitive, array, enum e any. I tipi primitivi sono:

TipoNoteEsempio
booleanvero o falsovar exists: boolean = true
numbernessuna separazione tra integer e float/double: tutti i numeri sono valori floating pointvar x: number = 55 · var y: number = 123.4567
stringsingoli o doppi apici indifferentemente; non esiste un tipo charvar msg = 'hello world' · "hello world"
voidusato principalmente per indicare una funzione che non restituisce valori
null / undefinedi due "non valori" di JavaScript
Attenzione

Le slide dedicano una domanda-retorica ai numeri: quanto fa 0.1 + 0.2? In JavaScript (e quindi in TypeScript, che ne eredita i numeri) il risultato è 0.30000000000000004, e (0.1 + 0.2) == 0.3 è false. La rappresentazione floating point non è un bug da "aggiustare": è un fatto che ogni sviluppatore web deve conoscere quando confronta decimali.

6. Array, Enum, Any e type inference

Gli array si annotano con il tipo seguito da parentesi quadre: var cities: string[] = ['Cesena','Bologna','Ravenna'];, var primes: number[] = [2,3,5,7,11,13,17,23];, var bools: boolean[] = [true, true, false, true];.

Gli Enum sono simili all'Enum di C#: un modo per definire un insieme di elementi. Sono 0-indexed, ma è possibile specificare l'indice per ogni elemento: enum Color{Red, Green, Blue} e var b: Color = Color.Red;, oppure enum Color{Red = 1, Green, Blue} per partire da 1.

any è utile per descrivere il tipo di variabili che potremmo non sapere quando scriviamo l'applicazione — ad esempio l'input dell'utente o una libreria di terze parti — e consente di rinunciare al type-checking: var notSure: any; può ricevere 2 e poi 'hello' senza errori, e var list: any[] = [1, true, 'hi']; ammette list[2] = 42;.

TypeScript cerca inoltre di inferire il tipo di una variabile: dato var x = 175, il tipo è number, e assegnare x = 'hello' produce Cannot convert 'string' to 'number'. I quattro modi per dichiarare una variabile sono riassunti nella tavola seguente.

7. Funzioni

Le funzioni sono una componente fondamentale di qualsiasi applicazione JavaScript, e JavaScript supporta le first-class function. TypeScript aggiunge alle funzioni standard di JavaScript tre funzionalità: tipi per parametri e tipo di ritorno, parametri opzionali e di default e function overload.

// Tipi per parametri e tipo di ritorno
function add(x: number, y: number): number {
  return x + y;
}
add(2, 3);           // ok
add(2, 'Hello');     // errore
add('hello', 'world'); // errore
// return 'hello';   // errore

// Parametri opzionali e di default
function display(msg: string, user: string = 'du') {
  alert(msg + ' ' + user);
}
display('hello');        // hello du
display('hello', 'world'); // hello world

// Function overload
function add(var1: string, var2: string): string;
function add(var1: number, var2: number): number;
function add(var1: any, var2: any): any {
  if (typeof var1 == 'number' && typeof var2 == 'number') {
    return var1 + var2;
  }
  if (typeof var1 == 'string' && typeof var2 == 'string') {
    return var1 + ' ' + var2;
  }
}
var r1 = add(10, 20);        // 30
var r2 = add('hello', 'world'); // hello world

I parametri opzionali dovrebbero avere un valore di default da usare se il valore non è specificato nell'invocazione della funzione. L'overload dichiara più firme per la stessa funzione: il corpo è unico e decide il comportamento a runtime in base ai tipi effettivi.

8. Classi

Le classi TypeScript combinano la sintassi di JavaScript con la disciplina dei linguaggi a tipizzazione statica. Le slide mostrano field e property: un campo può essere public — name: string — o private con un backing fieldprivate _salary: number — e le property si definiscono con get/set, dove il setter può validare il valore:

class Employee {
  name: string;              // public field
  private _hiddenField: string; // private field
  private _salary: number;   // private backing field

  // public property
  get Salary(): number {
    return this._salary;
  }
  set Salary(value: number) {
    if (value <= 0) {
      throw "salary can not be less than 0";
    } else {
      this._salary = value;
    }
  }
}

var emp: Employee = new Employee();
try { emp.Salary = -100; } catch (error) { alert(error); }
emp.Salary = 50;
alert(emp.Salary);

Il costruttore si definisce con la keyword constructor: è public di default e non può essere private. I metodi possono essere public o private, e i metodi statici si chiamano sul nome della classe: Employee.staticMethod(), mentre quelli di istanza si chiamano sull'oggetto — new Employee().instanceMethod().

TypeScript supporta l'ereditarietà attraverso la keyword extends, con super() che chiama il costruttore della classe base. Nell'esempio delle slide, Person è la base class con costruttore constructor(public name: string, public age: number) e metodo showInfo(); Employee extends Person aggiunge salary, chiama super(name, age) e sovrascrive showInfo(). L'esempio finale mostra il polimorfismo: var emp: Person = new Employee('Bob', 55, 100); e emp.showInfo() chiama la versione di Employee.

9. Interfacce e moduli

Le interfacce sono definite con la keyword interface e, come altre funzionalità, sono considerate solo a design-time: se compilate, producono un file vuoto. Esempio delle slide:

interface Employee {
  FirstName: string;
  LastName: string;
}
var emp: Employee = { FirstName: "Mario", LastName: "Rossi" };
alert(emp.FirstName);

Le proprietà opzionali si dichiarano con ?: LastName?: string. Nell'esempio, ShowEmployeeDetails(emp1) funziona come previsto, passare una stringa produce undefined, mentre un oggetto senza LastName non genera errori proprio perché la proprietà è opzionale.

I moduli sono definiti con la keyword module e possono contenere moduli, classi, interfacce ed enum — ma non funzioni. Classi e interfacce possono essere esposte all'esterno usando la keyword export:

module Utils {
  export class Math {
    public add(x: number, y: number): number {
      return x + y;
    }
  }
  // not accessible outside the module
  class Helper {
    help() {}
  }
}
var m = new Utils.Math;
m.add(20, 30);

La classe Helper non è accessibile all'esterno del modulo: è il meccanismo di incapsulamento che JavaScript puro non offre.

10. Compilazione: tsc, tsconfig e il caso Greeter

A livello sintattico TypeScript non è particolarmente innovativo: uno dei vantaggi principali è la combinazione di tre elementi — Intellisense (il sistema di suggerimenti e autocompletamento che l'IDE fornisce durante la scrittura), type-checking statico (il tipo di una variabile è conosciuto a tempo di compilazione) e compilazione. Il compilatore tsc (TypeScript Compiler) produce codice JavaScript a partire da codice TypeScript, con tre modalità di compilazione: 1-a-1 di file (da .ts a .js), tutti i file internamente a una cartella, oppure un file sorgente e tutti i file referenziati da esso. La compilazione può avvenire manualmente o tramite IDE ed è configurata dal file tsconfig.json o da argomenti in ingresso all'eseguibile tsc.

L'esempio completo delle slide è il classico Greeter: una classe con greeting, un costruttore e un metodo greet(), più la creazione di un bottone che saluta al click. Le due colonne della slide mostrano il codice TypeScript e il corrispondente JavaScript compilato: le annotazioni di tipo spariscono, le classi diventano funzioni con prototype, e il codice resta plain JavaScript.

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");

let button = document.createElement('button');
button.textContent = "Say Hello";
button.onclick = function() {
  alert(greeter.greet());
}
document.body.appendChild(button);
var Greeter = /** @class */ (function () {
  function Greeter(message) {
    this.greeting = message;
  }
  Greeter.prototype.greet = function () {
    return "Hello, " + this.greeting;
  };
  return Greeter;
}());
var greeter = new Greeter("world");

var button = document.createElement('button');
button.textContent = "Say Hello";
button.onclick = function () {
  alert(greeter.greet());
};
document.body.appendChild(button);

Notate cosa è sparito e cosa è cambiato: il tipo string non c'è più, la classe è diventata una funzione con i metodi su prototype, e tutto il resto è rimasto identico — è questo il contratto del transpiling.

11. Integrazione con JavaScript e Typings

È possibile utilizzare JavaScript puro in TypeScript? : il compilatore TypeScript rileva il codice JavaScript presente nei sorgenti e lo riporta tale e quale nel file JavaScript compilato. È possibile mantenere i vantaggi di TypeScript anche usando librerie JavaScript pure? Dipende: una libreria scritta in JavaScript non ha informazioni di tipo, quindi il type-checking non può controllarla.

Il problema è risolto da Typings (https://github.com/typings/typings), un manager nato proprio per gestire le definizioni di TypeScript: mantiene un archivio (più d'uno) di definizioni mantenute dalla comunità, un progetto GitHub. Il prezzo della manutenzione comunitaria si vede nei possibili problemi: definizioni assenti, definizioni errate (che è peggio), definizioni non allineate con la latest build della libreria originale (punto di attenzione!) e più definizioni in archivi diversi — quale usare? In generale per le librerie/framework principali (ad esempio jQuery) le definizioni sono corrette; per librerie di nicchia o con poco seguito su GitHub è possibile avere problemi. Se non fossero disponibili sorgenti TypeScript nativi e Typings non fosse d'aiuto, le alternative sono due: scrivere le definizioni manualmente oppure utilizzare any.

L'installazione avviene con npm: npm install -g typescript e npm install -g typings. È il punto d'ingresso di tutto il resto: senza TypeScript non si compila Angular, e senza Typings non si integrano le librerie legacy.

Verifica le tue conoscenze

Quali sono le problematiche di JavaScript elencate all'inizio della lezione?

Nessuna compilazione; tipizzazione "dinamica"; variabili globali; Abstract Equality Comparison Algorithm; scope inconsistencies; modello prototype based; difficile organizzazione architetturale. Gli aspetti positivi: setup immediato, ecosistema enorme e la computazione lato client su browser.

Che cosa significa "JavaScript is assembly language for the web" e quali linguaggi compilano in JavaScript?

È la visione che accomuna i linguaggi alternativi che compilano in JavaScript: JavaScript è il linguaggio "di basso livello" universale del web, e i linguaggi più espressivi vi vengono tradotti. Esempi citati: CoffeeScript, TypeScript, ClojureScript, Haxe, Scala e Dart (di Google).

Definite TypeScript e il transpiling.

TypeScript è un typed superset of JavaScript che compila in plain JavaScript ("Any browser. Any host. Any OS. Open source"), creato da Microsoft con Anders Hejlsberg. Il transpiling è la compilazione source-to-source (da super-set a linguaggio di partenza), distinta dalla compilazione source-to-binary; il transpiler ha il duplice compito di controllo degli errori e di generazione del nuovo codice sorgente.

Perché l'annotazione del tipo è "puramente a design time"?

Perché nel codice JavaScript prodotto dal compilatore non viene inserito alcun codice aggiuntivo: var age: number = 40; diventa semplicemente var age = 40;. Il tipo serve al compilatore per controllare gli errori prima dell'esecuzione — var age: number = 'forty'; produce Cannot convert 'string' to 'number' — ma a runtime non esiste più.

Descrivete i tipi primitivi di TypeScript e il caso 0.1 + 0.2.

boolean (true/false); number (nessuna separazione tra integer e float: tutti i numeri sono floating point); string (singoli o doppi apici, nessun tipo char); void (funzioni senza ritorno); null e undefined. Nel caso dei decimali: 0.1 + 0.2 vale 0.30000000000000004 e il confronto con 0.3 è false per la rappresentazione floating point.

Che cosa sono Array, Enum e Any in TypeScript?

Array: tipo seguito da parentesi quadre, var cities: string[] = [...];. Enum: insieme di elementi 0-indexed, con possibilità di specificare l'indice (enum Color { Red = 1, Green, Blue }), simili all'Enum di C#. Any: tipo per variabili di tipo sconosciuto (input utente, librerie di terze parti) che rinuncia al type-checking; utile anche come any[].

Che cosa fa la type inference e quali sono i quattro modi di dichiarare una variabile?

La type inference cerca di dedurre il tipo dal valore: var x = 175 è number, e assegnare x = 'hello' produce un errore. I quattro modi: 1) tipo e valore insieme (var message1: string = 'hello world'); 2) solo tipo, valore undefined; 3) solo valore, tipo inferito; 4) né tipo né valore: Any e undefined.

Quali funzionalità TypeScript aggiunge alle funzioni JavaScript?

Tre: tipi per parametri e tipo di ritorno (errori su chiamate non conformi); parametri opzionali e di default (il default si usa quando il valore non è specificato); function overload (più firme per la stessa funzione, corpo unico che decide a runtime, ad esempio somma numerica o concatenazione di stringhe).

Come funzionano classi, ereditarietà, interfacce e moduli in TypeScript?

Classi: field public/private, property con get/set (con validazione), costruttore (public di default), metodi statici e di istanza. Ereditarietà: keyword extends con super() per il costruttore della base, override dei metodi e polimorfismo. Interfacce: keyword interface, solo design-time (compilano in file vuoto), proprietà opzionali con ?. Moduli: keyword module, contengono moduli/classi/interfacce/enum ma non funzioni, con export per esporre e incapsulamento per il resto.

Come avviene la compilazione e come si integra TypeScript con le librerie JavaScript?

La compilazione è svolta da tsc, configurato da tsconfig.json o argomenti, con tre modalità (1-a-1, tutti i file di una cartella, file e referenziati). Il JavaScript puro viene riportato tale e quale nel file compilato. Per i vantaggi di TypeScript sulle librerie JS pure servono le Typings, definizioni mantenute dalla comunità: per le librerie principali sono corrette, per quelle di nicchia possono mancare, essere errate o non allineate; in alternativa si scrivono a mano o si usa any.