back to top

Moduli e namespace in TypeScript

In quest’ultima lezione parleremo brevemente dei due diversi metodi messi a disposizione da TypeScript per meglio organizzare e strutturare un’applicazione. Si tratta dei namespace e dei moduli che forniscono due approcci differenti.

Namespace in TypeScript

Iniziamo a parlare dei namespace che rappresentano uno dei modi per organizzare il codice. I namespace, così come i moduli, hanno l’obiettivo primario di isolare variabili, funzioni o classi in essi definiti dal resto del codice. Questi ultimi sono accessibili solo all’interno del loro namespace a meno che non vengono esplicitamente esportati e resi visibili anche all’esterno. Organizzando il codice in questo modo, ogni elemento di un namespace ha visibilità limitata al namespace stesso. Si evita così di creare variabili globali non necessarie che possono causare problemi di efficienza in termini di memoria. Le variabili globali restano infatti in memoria finché l’applicazione è in esecuzione e non vengono distrutte dal garbage collector. Un altro problema, che i namespace cercano di risolvere, è rappresentato dal rischio di collisione dei nomi di variabili che sono definite nello stesso blocco. Isolando delle variabili all’interno di un namespace diminuisce il rischio di avere due o più variabili all’interno di un blocco con lo stesso nome. Grazie ai namespace è possibile inoltre abbassare la probabilità che una funzione venga ridefinita in qualche altra parte della nostra applicazione dando vita a un comportamento inaspettato.

Vediamo un primo esempio in cui creiamo un namespace come mostrato nel frammento di codice riportato sotto.

// file: app.ts

namespace StringUtils {

  function stringArrayToUpperCase(arr: string[]): string[] {
    return arr.map(str => str.toUpperCase());
  }

  export function splitByN(str: string, n: number): string[] {
    n = n > 0 ? n : 1;
    const pattern = `.{1,${n}}`;
    const regex = new RegExp(pattern, 'g');
    let array = str.match(regex);
    array = array ? <string[]>array : [];
    return array;
  }

  export function charCodes(word: string): number[] {
    return word.split('').map(char => char.charCodeAt(0));
  }
}

const word = 'ciao';
const arr = StringUtils.splitByN(word, 0);
console.log(arr); // [ 'c', 'i', 'a', 'o' ]
console.log(StringUtils.charCodes(word)); // [ 99, 105, 97, 111 ]
// Property 'stringArrayToUpperCase' does not exist on type 'typeof StringUtils'
console.log(StringUtils.stringArrayToUpperCase(arr));

Inseriamo il frammento di codice riportato sopra all’interno di un file app.ts. Per creare un namespace usiamo l’omonima keyword. All’interno delle parentesi graffe definiamo alcune funzioni (avremmo potuto definire anche classi o varibili/costanti). Abbiamo usato la keyword export per esportare le funzioni che devono essere visibili all’esterno del namespace. Per esempio la funzione StringUtils.stringArrayToUpperCase() non è visibile all’esterno del namespace StringUtils.

È interessante mostrare il file .js generato da TypeScript dopo la compilazione. Il nuovo file creato usa il Module Pattern. Grazie alla IIFE (immediately-invoked function expression), le funzioni in essa definite non sono direttamente accessibili all’esterno. Solo le due funzioni StringUtils.charCodes() e StringUtils.splitByN() sono visibili all’esterno.

// file app.js generato dal comando 'tsc app.ts --target ES5'
var StringUtils;
(function (StringUtils) {
  function stringArrayToUpperCase(arr) {
    return arr.map(function (str) { return str.toUpperCase(); });
  }
  function splitByN(str, n) {
    n = n > 0 ? n : 1;
    var pattern = ".{1," + n + "}";
    var regex = new RegExp(pattern, 'g');
    var array = str.match(regex);
    array = array ? array : [];
    return array;
  }

  // rende la funzione splitByN() accessibile all'esterno
  StringUtils.splitByN = splitByN;
    
  function charCodes(word) {
    return word.split('').map(function (char) { return char.charCodeAt(0); });
  }

  // rende la funzione charCodes() accessibile all'esterno
  StringUtils.charCodes = charCodes;

})(StringUtils || (StringUtils = {}));

var word = 'ciao';
var arr = StringUtils.splitByN(word, 0);
console.log(arr);
console.log(StringUtils.charCodes(word));
// Property 'stringArrayToUpperCase' does not exist on type 'typeof StringUtils'
console.log(StringUtils.stringArrayToUpperCase(arr));

Nell’esempio appena visto abbiamo definito, all’interno di un unico file, il namespace StringUtils e usato le funzioni in esso presenti. Possiamo però inserire la definizione del namespace in un file diverso da quello in cui verrà poi usato. Riscriviamo allora l’esempio visto in precedenza. Creiamo due file lib.ts e app.ts come mostrato sotto.

// lib.ts

namespace StringUtils {

  function stringArrayToUpperCase(arr: string[]): string[] {
    return arr.map(str => str.toUpperCase());
  }

  export function splitByN(str: string, n: number): string[] {
    n = n > 0 ? n : 1;
    const pattern = `.{1,${n}}`;
    const regex = new RegExp(pattern, 'g');
    let array = str.match(regex);
    array = array ? <string[]>array : [];
    return array;
  }

  export function charCodes(word: string): number[] {
    return word.split('').map(char => char.charCodeAt(0));
  }
}
// app.ts

/// <reference path="lib.ts" />

const word = 'ciao';
const arr = StringUtils.splitByN(word, 0);
console.log(arr);
console.log(StringUtils.charCodes(word));

Nel file lib.ts abbiamo inserito la definizione del namespace StringUtils, nel file app.ts usiamo invece il namespace appena creato. All’inizio del file app.ts abbiamo utilizzato quella che prende il nome di Triple-Slash Directive che va inserita all’inizio del file. Si tratta di un commento su una sola riga contenente un tag XML. Nel nostro caso usiamo l’attributo path per indicare il percorso del file lib.ts da cui dipende il file app.ts. Tale direttiva viene usata dal compilatore per includere i file necessari nella fase di compilazione.

A questo punto possiamo generare i file .js in due modi. Possiamo creare due file distinti lib.js e app.js con il comando riportato sotto. (Ovviamente eventuali opzioni da usare in fase di compilazioni vanno aggiunte al comando riportato sotto o inserite in un file tsconfig.json)

$ tsc app.ts lib.ts
// lib.js

var StringUtils;
(function (StringUtils) {
  function stringArrayToUpperCase(arr) {
    return arr.map(function (str) { return str.toUpperCase(); });
  }
  function splitByN(str, n) {
    n = n > 0 ? n : 1;
    var pattern = ".{1," + n + "}";
    var regex = new RegExp(pattern, 'g');
    var array = str.match(regex);
    array = array ? array : [];
    return array;
  }
  StringUtils.splitByN = splitByN;
  function charCodes(word) {
    return word.split('').map(function (char) { return char.charCodeAt(0); });
  }
  StringUtils.charCodes = charCodes;
})(StringUtils || (StringUtils = {}));
// app.js

/// <reference path="lib.ts" />

var word = 'ciao';
var arr = StringUtils.splitByN(word, 0);
console.log(arr);
console.log(StringUtils.charCodes(word));

Verranno creati due file distinti. Sarà poi nostro compito inserirli per esempio all’interno di una pagina HTML con due elementi <script>.

<!-- index.html -->
<script src="lib.js" type="text/javascript" />
<script src="app.js" type="text/javascript" />

Nel corso di questa guida abbiamo usato spesso Node.js per eseguire i file .js generati dal transpiler. Se volessimo eseguire il file app.js appena creato, visualizzeremmo un messaggio di errore.

$ node app.js
var arr = StringUtils.splitByN(word, 0);
                      ^

ReferenceError: StringUtils is not defined

Il motivo è che la Triple-Slash Directive viene usata da TypeScript, ma per il motore Javascript si tratta semplicemente di un commento. Node.js non è in grado di sapere cosa sia StringUtils. Al contrario, inserendo i due script in un file HTML, StringUtils ha visibilità globale. Per eseguire lo script in Node.js possiamo usare il sistema dei moduli che approfondiremo a breve. Per esempio possiamo usare la specifica CommonJS solitamente impiegata in Node.js. Modifichiamo allora i due file come segue.

// app.ts

import {StringUtils} from './lib';

const word = 'ciao';
const arr = StringUtils.splitByN(word, 0);
console.log(arr);
console.log(StringUtils.charCodes(word));
// lib.ts

// esportiamo il namespace 
export namespace StringUtils {

  function stringArrayToUpperCase(arr: string[]): string[] {
    return arr.map(str => str.toUpperCase());
  }

  export function splitByN(str: string, n: number): string[] {
    n = n > 0 ? n : 1;
    const pattern = `.{1,${n}}`;
    const regex = new RegExp(pattern, 'g');
    let array = str.match(regex);
    array = array ? <string[]>array : [];
    return array;
  }

  export function charCodes(word: string): number[] {
    return word.split('').map(char => char.charCodeAt(0));
  }
}

All’inizio del file app.ts importiamo StringUtils da lib.ts nel quale abbiamo esportato il namespace (export namespace StringUtils{… }).

Il secondo modo per generare i file .js, consiste nel creare un unico file in cui vengono concatenati i due file lib.ts e app.ts. In questo caso il transpiler usa la Triple-Slash Directive per concatenare i file in ordine visto che app.ts dipende da lib.ts. Ripristiniamo quindi le versioni originali dei due file e lanciamo il comando riportato sotto.

$ tsc app.ts lib.ts --outFile app.js

In TypeScript è altresì possibile dividere la definizione di un namespace su più file e poi generare altrettanti file .js o compilarli in un unico file come abbiamo visto in precedenza. Anche in questo caso useremo la Triple-Slash Directive per esprimere la dipendenza fra i file.

// file: StringUtils.ts

/// <reference path="StringUtils.StringArrayToUpperCase.ts" />

namespace StringUtils {
  export function splitByN(str: string, n: number): string[] {
    n = n > 0 ? n : 1;
    const pattern = `.{1,${n}}`;
    const regex = new RegExp(pattern, 'g');
    let array = str.match(regex);
    array = array ? <string[]>array : [];
    return array;
  }

  export function charCodes(word: string): number[] {
    return word.split('').map(char => char.charCodeAt(0));
  }
}
// file: StringUtils.StringArrayToUpperCase.ts

namespace StringUtils {

  function stringArrayToUpperCase(arr: string[]): string[] {
    return arr.map(str => str.toUpperCase());
  }

}

Eseguiamo il comando tsc per compilare i file .ts.

$ tsc --outFile StringUtils.js \
  StringUtils.StringArrayToUpperCase.ts StringUtils.ts

Il file StringUtils.js risultante sarà simile a quello riportato sotto.

// StringUtils.js

var StringUtils;
(function (StringUtils) {
    function stringArrayToUpperCase(arr) {
        return arr.map(function (str) { return str.toUpperCase(); });
    }
})(StringUtils || (StringUtils = {}));
/// <reference path="StringUtils.StringArrayToUpperCase.ts" />
var StringUtils;
(function (StringUtils) {
    function splitByN(str, n) {
        n = n > 0 ? n : 1;
        var pattern = ".{1," + n + "}";
        var regex = new RegExp(pattern, 'g');
        var array = str.match(regex);
        array = array ? array : [];
        return array;
    }
    StringUtils.splitByN = splitByN;
    function charCodes(word) {
        return word.split('').map(function (char) { return char.charCodeAt(0); });
    }
    StringUtils.charCodes = charCodes;
})(StringUtils || (StringUtils = {}));

I moduli in TypeScript

Abbiamo brevemente accennato come sia possibile usare i moduli in TypeScript in uno dei precedenti esempi sui namespace. I moduli costituiscono il secondo metodo messo a disposizione da TypeScript per l’organizzazione del codice. I moduli cercano di risolvere gli stessi problemi che abbiamo esposto nel caso dei namespace, ma lo fanno utilizzando un approccio diverso. TypeScript permette di generare dei file .js compatibili con diverse specifiche. Nell’esempio precedente abbiamo usato l’opzione standard e abbiamo generato dei moduli compatibili con CommonJS. È però possibile specificare un diverso formato come quello dei moduli definito in ES2015. Al di là dei file generati dal transpiler, la sintassi da usare in TypeScript è sempre la stessa ed è sostanzialmente quella di ES2015. Ogni modulo viene definito in un unico file. Le funzioni, variabili o classi presenti in un modulo hanno visibilità limitata al modulo stesso e per renderle accessibili ad altri moduli devono essere esportate attraverso la keyword export. Verranno quindi importate usando import come vedremo negli esempi riportati di seguito.

Riprendiamo l’esempio già visto in precedenza per i namespace e riscriviamolo per illustrare come funzionano i moduli. Creiamo due file che chiamiamo StringUtils.ts e app.ts. Nel primo inseriamo le definizioni delle funzioni già esaminate sopra, nel secondo importiamo e usiamo le funzioni definite nel primo file.

// StringUtils.ts

function stringArrayToUpperCase(arr: string[]): string[] {
  return arr.map(str => str.toUpperCase());
}

export function splitByN(str: string, n: number): string[] {
  n = n > 0 ? n : 1;
  const pattern = `.{1,${n}}`;
  const regex = new RegExp(pattern, 'g');
  let array = str.match(regex);
  array = array ? <string[]>array : [];
  return array;
}

export function charCodes(word: string): number[] {
  return word.split('').map(char => char.charCodeAt(0));
}

Nel file StringUtils.ts abbiamo definito tre funzioni, ma solo due saranno visibili all’esterno del modulo, ovvero quelle precedute dalla keyword export. In alternativa possiamo elencare alla fine del file le funzioni che devono essere esportate. Come mostrato nell’esempio riportato sotto, abbiamo racchiuso l’elenco delle funzioni fra parentesi graffe. La funzione splitByN() sarà visibile all’esterno con il nome splitAWordEveryNChars() dato in fase di esportazione. In fase di importazione dovremo quindi usare il nuovo nome.

function stringArrayToUpperCase(arr: string[]): string[] {
  return arr.map(str => str.toUpperCase());
}

function splitByN(str: string, n: number): string[] {
  n = n > 0 ? n : 1;
  const pattern = `.{1,${n}}`;
  const regex = new RegExp(pattern, 'g');
  let array = str.match(regex);
  array = array ? <string[]>array : [];
  return array;
}

function charCodes(word: string): number[] {
  return word.split('').map(char => char.charCodeAt(0));
}

export { splitByN as splitAWordEveryNChars, charCodes };

Ogni modulo può inoltre esportare un elemento di default che sarà poi importato in maniera diversa rispetto agli altri elementi. In questo caso useremo la parola chiave default e non racchiuderemo il nome della funzione fra parentesi graffe.

function stringArrayToUpperCase(arr: string[]): string[] {
  return arr.map(str => str.toUpperCase());
}

function splitByN(str: string, n: number): string[] {
  n = n > 0 ? n : 1;
  const pattern = `.{1,${n}}`;
  const regex = new RegExp(pattern, 'g');
  let array = str.match(regex);
  array = array ? <string[]>array : [];
  return array;
}

function charCodes(word: string): number[] {
  return word.split('').map(char => char.charCodeAt(0));
}

export { splitByN as splitAWordEveryNChars, charCodes };
export default stringArrayToUpperCase;

Nel file app.ts potremo quindi importare le funzioni definite nel file StringUtils.js nel seguente modo:

// app.ts

import stringArrayToUpperCase, 
  { splitAWordEveryNChars as split, charCodes } from './StringUtils';

const word = 'ciao';
const arr = split(word, 0);

console.log(arr);
console.log(charCodes(word));

console.log(stringArrayToUpperCase(arr));

Abbiamo importato stringArrayToUpperCase() senza racchiuderla fra parentesi graffe dal momento che era stata esportata con la sintassi ‘export default…’ nel file StringUtils.ts. Abbiamo deciso di usare un alias in fase di importazione per splitAWordEveryNChars() per mostrare che è possibile rinominare una qualsiasi funzione, classe o variabile anche in fase di importazione.

A questo punto possiamo compilare i file .ts e generare dei moduli Javascript nel formato che più preferiamo. Di default, se il target delle impostazioni di compilazione è ‘ES3’ o ‘ES5’, verranno generati dei file compatibili con le specifiche di CommonJS. Potremo quindi usarli tranquillamente con Node.js che supporta tale formato. Se vogliamo usarli nel browser dovremo usare un opportuno strumento che permetta di caricare ed eseguire il codice così organizzato. Tools come Browserify o Webpack permettono di utilizzare i moduli nel formato CommonJS anche nel browser. In sostanza, TypeScript provvede a generare i moduli nel formato indicato, sarà poi nostra premura configurare l’ambiente di esecuzione in maniera tale che il codice in essi presenti possa essere eseguito.

$ tsc --module es2015 --target es2015 app.ts StringUtils.ts

In TypeScript è possibile generare anche dei moduli nel formato ES2015 che, come nel caso di CommonJS, possono essere usati attraverso strumenti come Webpack o direttamente, se supportati. Al momento della stesura di questa lezione, Node.js supporta i moduli ES2015 in via sperimentale. È comunque possibile iniziare a usarli lato client, visto che sono supportati dalla maggior parte dei browser moderni. Dovremo però adottare gli opportuni accorgimenti per garantire la compatibilità con i browser che non li supportano.

Conclusioni

In questa lezione abbiamo riassunto quelli che sono i due diversi metodi messi a disposizione da TypeScript per organizzare il codice di un progetto e evitare di affollare lo scope globale. Questa è l’ultima lezione di questa breve guida in cui abbiamo cercato di illustrare i concetti fondamentali per poter usare TypeScript nel più breve tempo possibile. Se avete dubbi o domande potete postare sul nsotro forum.

Pubblicitร 
Articolo precedente