Qualche settimana fa ho avuto modo di partecipare a un techbar organizzato da DevMarche e tenuto da Andrea Balducci su TypeScript (le slides), il “nuovo” linguaggio simil-javascript targato Microsoft e usato in AngularJS 2 da Google. Lo stesso Andrea ha ripreso il dicorso la settimana scorsa al MobileCamp, approfondendo alcuni concetti (le slides).
In realtà TypeScript non è nuovo per niente ma – sebbene abbia visto la luce a fine 2012 – soltanto con l’annuncio dell’adozione nella versione 2 di AngularJS ha ridestato l’interesse della comunità ed è salito agli onori della cronaca.
La (finta) concorrenza: CoffeeScript e Dart
Forse perché il Javascript è nato in pochi giorni e con un obiettivo totalmente diverso da quelli odierni, fatto sta che i più odiano alcune sue caratteristiche, e nel corso del tempo si è cercato di nasconderlo dietro qualche altro linguaggio così da mascherarne difetti e brutture.
La prima di queste “maschere” è stata CoffeeScript, un linguaggio “ponte” compilabile in Javascript che prese spunto prevalentemente da Ruby, Haskell e Python. Scopo primo: rendere il codice più compatto e leggibile.
Evidentemente questo “nuovo linguaggio” non deve aver fatto breccia a Mountain View, perché poco dopo è uscito Dart. Lo scopo di Google era abbastanza più sensibile: mandare in soffitta il Javascript come linguaggio del web.
L’esigenza manifestata da Google di “rompere con il passato” non deve però aver convinto quelli di Microsoft, che poco dopo hanno fatto la loro mossa sviluppando TypeScript.
A questo punto Google pensava di sviluppare un ulteriore superset di TypeScript chiamato AtScript – che aggiungeva principalmente le annotations al linguaggio di Microsoft. Fortunatamente Google ha deciso di abbandonare il progetto collaborando con Microsoft allo sviluppo di TypeScript.
Cos’è TypeScript
TypeScript è un “Superset tipizzato di Javascript che compila in Javascript”, ovvero è un Javascript ampliato con alcuni costrutti (il cui uso è facoltativo), che può essere compilato in Javascript da un compilatore anche questo scritto in TypeScript.
La novità è che il compilato è né più né meno che un bel Javascript, tendenzialmente identico a come l’avremmo scritto noi, e questo succede perché molte delle nuove “funzionalità” non vanno a generare un bel niente, ma servono soltanto al compilatore per una verifica statica del codice.
Naturalmente essendo il prodotto del compilatore del “codice Javascript”, tra i parametri di configurazione del compilatore si può specificare anche la versione di “Javascript” (o più propriamente EcmaScript), e i costrutti utilizzati vengono tradotti oppure no a seconda della versione di EcmaScript scelta.
Qualche nota sul compilatore
Installazione
Typescript è una libreria presente su npm che installa il compilatore TypeScript da usare globalmente, installiamolo quindi con:
npm install -g typescript
Esecuzione
Il compilatore si richiama con il comando:
tsc nomefile.ts
Il risultato è la creazione nella stessa directory del file .ts di un file Javascript con lo stesso nome ma con estensione js. Aggiungendo il parametro -w
(o --watch
) viene creato uno watcher sul file che lo ricompila a ogni modifica.
Per visualizzare una guida del compilatore eseguire il comando tsc -h
(o tsc --help
).
Configurazione
Di recente è stato introdotto il file di configurazione tsconfig.json
, la cui presenza marca la directory come un progetto TypeScript. Se il file è vuoto vengono usate le configurazioni di default e la compilazione viene eseguita su tutti i file ts trovati (basta lanciare il tsc
da solo, o eventualmente mettendolo in “watch”).
Nel repository GitHub di TypeScript è presente una guida esaustiva, elenco qui giusto alcune configurazioni principali:
- “files”: è un array opzionale contenente i file che devono essere considerati dal compilatore, in sua assenza vengono compilati tutti i file ts presenti nella directory e nelle subdirectories.
- “target” (“compilerOptions”): specifica la versione di EcmaScript di destinazione (“es3”, “es5” o “es6”)
- “sourceMap” (“compilerOptions”): specifica se devono essere generati i source map per poter eseguire il debug direttamente dello script TypeScript
- “module” (“compilerOptions”): specifica il formato dei moduli, il default è “commonjs”, il formato usato in NodeJS
Il “superset” di istruzioni
Sono pigro, e credo fermamente che re-inventare sempre la ruota non serve. Prendo quindi gli esempi del workshop di cui sopra cercando di analizzarli per quanto mi è possibile.
Any? Un po’ troppo generico
function sortByName(a){ var result = a.slice(0); result.sort(function(x,y){ return x.name.localCompare(y.name); }) } sortByName(5);
In questo esempio vediamo una semplice funzione di ordinamento, e la sua invocazione (sbagliata). Dichiarando qualche tipo i bug saltano fuori in un attimo, specialmente utilizzando une editor come Visual Studio Code, che sembra tagliato su misura per sviluppare in TypeScript…
function sortByName(a:any[]){ var result = a.slice(0); result.sort(function(x,y){ return x.name.localeCompare(y.name); }) } sortByName([{'name' : '5'}]);
function sortByName(a:{'name':string}[]){ var result = a.slice(0); result.sort(function(x,y){ return x.name.localeCompare(y.name); }) } sortByName([{'name' : '5'}]);
interface Named { 'name' : string } function sortByName(a:Named[]):Named[]{ var result = a.slice(0); result.sort(function(x,y){ return x.name.localeCompare(y.name); }); return result; } var data = [{'name' : '5'}]; var sorted = sortByName(data); console.log(sorted);
function sortByName(a) { var result = a.slice(0); result.sort(function (x, y) { return x.name.localeCompare(y.name); }); return result; } var data = [{ 'name': '5' }]; var sorted = sortByName(data); console.log(sorted);
Come si può vedere da questo semplice esempio il compilatore usa buona parte di quello che scriviamo per fare controllo statico del codice, e il risultato non è minimamente alterato dalla definizione di qualche contratto sui tipi.
Classi e interfacce per farci sentire a casa
In quest’esempio vediamo una semplice classe con una proprietà privata (level), due proprietà pubbliche (name e bio) e un metodo pubblico (train). Il dichiarare come pubblici i parametri del costruttore li identifica automaticamente come proprietà pubbliche della classe.
export interface Named { name: string; } export class Jedi implements Named { private level:number = 0; constructor(public name: string, public bio: string) { this.level = 15; } train(levelUp : number){ this.level += levelUp; return this.level; } }
var Jedi = (function () { function Jedi(name, bio) { this.name = name; this.bio = bio; this.level = 0; this.level = 15; } Jedi.prototype.train = function (levelUp) { this.level += levelUp; return this.level; }; return Jedi; })(); exports.Jedi = Jedi;
Come si può vedere il risultato non aggiunge niente al javascript, trattasi sempre di una funzione Jedi che nel prototype ha la funzione dichiarata come metodo pubblico.
Naturalmente così come si può implementare un’interfaccia con implements
si può anche estendere un’altra classe con extends
.
L’uguaglianza tra tipi si basa su un confronto delle interfacce tra i vari oggetti (interfacce intese come proprietà e metodi esposti), quindi su questo fronte potrebbero esserci dei fraintendimenti se si è abituati a lavorare con linguaggi tipizzati come Java e C#.
I Generics per svincolarci dai tipi
Il TypeScript è nato principalmente per rendere più facile la vita degli sviluppatori Java/C#/simili nel mondo Javascript, e in quei linguaggi per aumentare la riusabilità del codice esistono i generics, giocoforza dovevano gestirli anche qui.
Una guida esaustiva ai generics in TypeScript la potete trovare qui, mi limito a riportare un esempio preso dalla guida perché l’argomento è lungo e complesso.
interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); return arg; } loggingIdentity({length: 10, value: 3}); loggingIdentity([3]); loggingIdentity(3); // Errore di compilazione
Decorators per una facile estendibilità
Tra tutte forse la novità che ha colpito maggiormente l’interesse degli sviluppatori è quella dei decorators. Il pattern Decorator è uno dei più utilizzati e permette di “aumentare” le funzionalità di un oggetto a runtime “avvolgendolo”, ovvero funzionando da wrapper.
I decorators TypeScript sono delle annotations applicabili – eventualmente in sequenza – a delle dichiarazioni di classi, funzioni, proprietà (di classe) e argomenti (di funzione) per estenderne il comportamento.
Una serie di articoli molto completa e interessante che spiega come funzionano i Decorators in TypeScript potete trovarla qui. Per arrivare in fondo a quest’articolo prima dell’inverno anche qui eviterò di dilungarmi, riportando giusto il classico esempio usato da tutti: il log di una chiamata di funzione.
export function log(target: Function, key: string, descriptor: any) { var original = descriptor.value; descriptor.value = function(...args: any[]) { var a = args.map(a => JSON.stringify(a)).join(); var result = original.apply(this, args); var r = JSON.stringify(result); console.log(`call: ${key}(${a}) => ${r}`); return result; } return descriptor; }
import {log} from "./decorators" export class Jedi { private level:number = 0; constructor(public name: string, public bio: string) { this.level = 15; } @log train(levelUp : number){ this.level += levelUp; return this.level; } }
if (typeof __decorate !== "function") __decorate = function (decorators, target, key, desc) { if (typeof Reflect === "object" && typeof Reflect.decorate === "function") return Reflect.decorate(decorators, target, key, desc); switch (arguments.length) { case 2: return decorators.reduceRight(function(o, d) { return (d && d(o)) || o; }, target); case 3: return decorators.reduceRight(function(o, d) { return (d && d(target, key)), void 0; }, void 0); case 4: return decorators.reduceRight(function(o, d) { return (d && d(target, key, o)) || o; }, desc); } }; var decorators_1 = require("./decorators"); var Jedi = (function () { function Jedi(name, bio) { this.name = name; this.bio = bio; this.level = 0; this.level = 15; } Jedi.prototype.train = function (levelUp) { this.level += levelUp; return this.level; }; Object.defineProperty(Jedi.prototype, "train", __decorate([ decorators_1.log ], Jedi.prototype, "train", Object.getOwnPropertyDescriptor(Jedi.prototype, "train"))); return Jedi; })(); exports.Jedi = Jedi;
Facciamola breve: la funzione __decorate
prende il prototype della classe contenente la funzione che vogliamo decorare e “avvolge” la funzione scelta con le funzioni “decoratrici” passate nell’array, in sequenza. Basta annotare cioè due volte la stessa funzione con lo stesso decorator @log
per ritrovarci con il decorators_1.log
due volte nell’array, e per vedere due volte il log dell’invocazione della funzione.
L’Object.defineProperty
sovrascrive la funzione originaria con quella decorata ritornata dal __decorate
.
La prima parte può spaventare un po’, ma in realtà sta soltanto definendo la funzione __decorate
in modo da farle usare il Reflect.decorate
se disponibile o farle implementare la logica di decorazione in caso contrario.
Modules, per “modularizzare” meglio il codice
Sistemi per rendere più modulare il nostro codice Javascript c’erano anche senza scomodare TypeScript, ma i moduli sono comunque un’opportunità da non ignorare.
module Validation { export interface StringValidator { isAcceptable(s: string): boolean; } } module Validation { var lettersRegexp = /^[A-Za-z]+$/; export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } } }
In sostanza un modulo serve ad aggregare funzioni affini in contenitori appropriati per evitare collisioni di nomi e altri errori. Lo stesso modulo può essere “riempito” anche da file diversi, così ad esempio se abbiamo tutta una serie di funzioni di validazione e non vogliamo creare file enormi possiamo spezzettare il modulo in più file.
Anche in questo caso ho tutt’altro che approfondito, si può trovare una guida esaustiva qui.
Utility varie, anche un po’ di zucchero non guasta
Let
test1 = "a"; // errore, la variabile non è stata ancora dichiarata let test1; if (true){ let test2; test2="a"; } test2="b"; // errore, la variabile è stata dichiarata internamente al blocco if
Il compilato Javascript di questo blocco è identico ma usa i var al posto dei let (se non stiamo compilando il EcmaScript 6 o superiori). Questo significa che il javascript funzionerebbe comunque effettuando l’hoisting delle variabili.
For of
for (var c of characters) { console.log(c); } // il compilato di questo ciclo è il seguente: for (var _i = 0; _i < characters.length; _i++) { var c = characters[_i]; console.log(c); }
Questa cosa è di una banalità estrema e non snellisce nemmeno di tanto il codice, ma confesso che a me almeno una volta è successo di aver dimenticato che usando ‘in’ la variabile viene valorizzata con l’indice e non con il valore…
Operatore freccia
class Test { private acc = ""; conc = (arg:string)=>{ this.acc = arg + " " + this.acc; return this.acc; } } // il compilato è: var Test = (function () { function Test() { var _this = this; this.acc = ""; this.conc = function (arg) { _this.acc = arg + " " + _this.acc; return _this.acc; }; } return Test; })();
Quest’ultimo esempio rende l’idea di come alcuni trabocchetti possano essere schivati usando certi costrutti, che aiutano anche a rendere più leggibile il codice e non riducono minimamente la retrocompatibilità del nostro codice.
Definizione di tipi per librerie esistenti?
Chi più chi meno tutti usiamo delle librerie/plugin e certo per utilizzare con profitto un linguaggio che si chiama “TypeScript” sarebbe bellissimo avere a disposizione le definizione dei tipi utilizzati in queste librerie.
Poco meno di tre anni fa tale Boris Yankov ebbe la bella idea di crearsi un repository su GitHub chiamato DefinitelyTyped e inizio a buttarci dentro le definizioni di alcune librerie Javascript.
Ebbene questo progetto ha da poco superato i 10000 commits e i 1000 contributori, e dentro ci si può trovare di tutto…
Conclusioni
Vero che per il teorema di Bohm-Jacopini qualunque algoritmo può essere scritto usando soltanto iterazione, struttura condizionale e sequenza, ma certo se si vuole realizzare grossi sistemi qualche costrutto un po’ più avanzato ce lo vuole.
Almeno se chi sviluppa ha una buona conoscenza informatica il sistema guadagna molto in eleganza e manutenibilità strutturandolo a dovere, e TypeScript da questo punto di vista può aiutare moltissimo laddove il Javascript (almeno inteso come Ecmascript 5 e inferiori) ha delle grosse lacune.
Per la maggior parte i costrutti aggiunti comportano solo lavoro per il compilatore e lasciano inalterato il codice risultante, in altri casi vengono tradotti esattamente come li tradurremmo noi se conoscessimo il linguaggio alla perfezione, cosa spesso non vera.
Benefici apportati dall’utilizzo di questo compilatore:
- l’architetto che può sfruttare costrutti avanzati per evitare di insozzare il codice
- chi sviluppa può farlo senza timori di pestare una mina a ogni riga perché il compilatore segnala gran parte degli errori di codifica senza dover eseguire il codice, rendendo più facile il refactoring (anche grazie a strumenti più avanzati) e limitando i bug da disattenzione
- l’interprete si trova a dover eseguire un codice migliore e ottimizzato, e può quindi raggiungere prestazioni migliori
Aspetti negativi di questo strumento:
- si deve aggiungere uno “strato” di codice, e questo potrebbe portare a qualche problemino con sdk particolari
- non mi viene in mente nient’altro
Ma a parte tutto secondo me il maggior merito di TypeScript (e della Microsoft) è un altro. Da più fronti si stava cercando di attaccare il Javascript per sostituirlo – sul web in particolare – senza dargli il tempo di crescere nelle varie versioni di EcmaScript. TypeScript sta riuscendo a spingere nella standardizzazione dei costrutti velocizzando i rilasci delle versioni EcmaScript ma dando l’opportunità a chi non può evolvere verso le nuove versioni di sfruttare già oggi tutti quei costrutti, e mantenendo una non trascurabile retrocompatibilità. Per me può bastare.