Per una migliore organizzazione del codice di un'applicazione TypeScript, oltre ai namespace, possiamo utilizzare i moduli. L'approccio adottato da TypeScript è praticamente quello definito dalle specifiche ECMAScript 6. Iniziamo dai fondamentali:
- Un modulo è contenuto in un file ed un file può contenere un solo modulo;
- Un modulo ha un proprio scope privato non accessibile dall'esterno.
Come per i namespace, soltanto gli elementi esplicitamente esportati tramite la parola chiave export vengono resi accessibili all'esterno del modulo. Analogamente, possiamo importare elementi da un modulo tramite la parola chiave import.
Un qualsiasi file che contiene un'istruzione import o export viene considerato un modulo.
Realizzare il primo modulo
Per definire un modulo che esporta una funzione per il calcolo dell'area di un triangolo è sufficiente inserire in un file il seguente codice:
export function areaTriangolo(base:number, altezza:number) {
return base * altezza / 2;
}
Supponiamo di aver memorizzato il codice precedente in un file geometria.js
. La funzione sarà disponibile all'esterno del modulo tramite l'istruzione import
:
import {areaTriangolo} from "./geometria"
console.log(areaTriangolo(7, 12));
export e import, esporre e usare gli elementi di un modulo
Un modulo può esportare più elementi, come nel seguente esempio:
export function areaTriangolo(base:number, altezza:number) {
return base * altezza / 2;
}
export function areaQuadrato(lato:number) {
return lato * lato;
}
export function areaCerchio(raggio:number) {
return 3.14 * raggio * raggio;
}
Ma un altro modulo può decidere di importare soltanto quello che gli serve:
import {areaTriangolo, areaCerchio} from "./geometria"
console.log(areaTriangolo(7, 12));
console.log(areaCerchio(5));
L'esportazione di elementi può avvenire anche con una dichiarazione a sé stante, come mostrato di seguito:
function areaTriangolo(base:number, altezza:number) {
return base * altezza / 2;
}
function areaQuadrato(lato:number) {
return lato * lato;
}
function areaCerchio(raggio:number) {
return 3.14 * raggio * raggio;
}
export {areaTriangolo};
export {areaQuadrato};
export {areaCerchio};
Ed è anche possibile esportare un elemento con un nome diverso da quello attribuito all'interno del modulo:
function areaCerchio(raggio:number) {
return 3.14 * raggio * raggio;
}
export {areaCerchio as calcolaAreaCerchio};
Anche nell'importazione da un modulo abbiamo la possibilità di rinominare gli elementi:
import {areaTriangolo as calcolaAreaTriangolo} from "./geometria"
console.log(calcolaAreaTriangolo(7, 12));
Mentre possiamo specificare di voler importare tutti gli elementi esportati da un modulo con il seguente codice:
import * as geometria from "./geometria"
console.log(geometria.areaTriangolo(7, 12));
In questo caso occorre specificare un alias per il modulo importato, geometria
nel nostro esempio, ed utilizzare l'alias per accedere agli elementi esportati dal modulo.
Se il nostro modulo esporta soltanto un elemento, allora possiamo utilizzare la clausola default, come mostrato nel seguente esempio:
export default function areaCerchio(raggio:number) {
return 3.14 * raggio * raggio;
}
In questo caso possiamo importare l'elemento specificando il nome da attribuire all'elemento esportato senza necessità delle parentesi graffe:
import areaDelCerchio from "./geometria"
console.log(geometria.areaDelCerchio(3));
Come possiamo vedere, abbiamo importato con il nome areaDelCerchio
la funzione di default definita come areaCerchio
. Dal momento che nel modulo esportiamo un solo elemento, possiamo anche anche non assegnargli un nome. Ad esempio, la seguente esportazione è del tutto equivalente a quella vista prima:
export default function (raggio:number) {
return 3.14 * raggio * raggio;
}
Moduli, loader e generazione di codice
La definizione di un modulo non indica di per sé la modalità di caricamento nell'ambiente di esecuzione. Questa dovrebbe far parte dei compiti di un componente del runtime detto loader e potrebbe cambiare proprio in base allo specifico ambiente di esecuzione. Tuttavia, proprio per l'assenza di supporto nativo ai moduli in JavaScript, prima della pubblicazione di ES6, si sono diffusi diversi approcci alla definizione e al caricamento dei moduli, molti dei quali propongono una propria sintassi non compatibile con quella degli altri.
Attualmente gli approcci più diffusi per la definizione ed il caricamento di moduli in JavaScript sono:
- Asynchronous Module Definition (AMD)
prevede un caricamento asincrono dei moduli ed è essenzialmente pensato per i browser; - CommonJS
prevede un caricamento sincrono dei moduli ed è adatto per la gestione lato server, come ad esempio con Node.js.
Esiste anche un approccio che, non senza difficoltà, prova ad unificare gli approcci Common JS e AMD ed è noto come Universal Module Definition.
Dal momento che il compilatore di TypeScript genera codice JavaScript, è interessante capire per che tipo di sintassi viene generata e per che tipo di loader è previsto il codice? In realtà il compilatore TypeScript può generare codice JavaScript compatibile con CommonJS, e quindi Node.js, con AMD, con UMD, SystemJS o moduli nativi ES6. È sufficiente specificare il tipo di output preferito come nei seguenti esempi:
tsc --module commonjs myModulo.ts
tsc --module amd myModulo.ts
Nel primo caso verrà generato codice con Node.js, mentre nel secondo caso verrà generato codice che prevede l'uso di Require.js, il loader AMD più noto.