Nel frenetico panorama digitale di oggi, JavaScript è diventato il linguaggio di riferimento per la creazione di applicazioni web dinamiche. Tuttavia, la tipizzazione dinamica di JavaScript può talvolta portare a errori impercettibili, difficili da individuare nelle prime fasi di sviluppo.

È qui che entra in gioco TypeScript, una tecnologia che rivoluziona il modo in cui scriviamo il codice JavaScript.

In questo articolo andremo alla scoperta di TypeScript e delle sue caratteristiche principali, i vantaggi e le best practice. Scopriremo come TypeScript rimuove i limiti di JavaScript e introduce la tipizzazione statica nello sviluppo di applicazioni web solide e scalabili.

Cominciamo!

Cos’è TypeScript?

TypeScript è un superset di JavaScript che aggiunge tipizzazione statica e funzioni avanzate a JavaScript. È stato sviluppato da Microsoft e rilasciato nell’ottobre del 2012. Dal momento del rilascio, si è rapidamente diffuso nella community del web development.

Secondo un sondaggio condotto nel 2022 da Stack Overflow, TypeScript è la quarta tecnologia più diffusa, con il 73,46% delle preferenze. TypeScript è stato creato per risolvere alcuni limiti di JavaScript, come la mancanza di tipizzazione forte, che può portare a errori impercettibili e difficili da individuare.

Si consideri, ad esempio, il seguente codice JavaScript:

function add(a, b) {
  return a + b;
}

let result = add(10, "20"); // No error, but result is "1020" instead of 30

Questo codice crea una funzione add, tipizzata dinamicamente. Il tipo degli argomenti a e b non viene imposto. Di conseguenza, passare una stringa invece di un numero come argomento non produce un errore. I valori sono concatenati come stringhe, provocando un comportamento inatteso.

Con TypeScript è stata introdotta la tipizzazione statica, che consente a chi sviluppa di specificare i tipi di variabili, parametri di funzione e valori di ritorno, individuando gli errori legati al tipo.

function add(a: number, b: number): number {
  return a + b;
}

let result = add(10, "20"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'

Nel codice TypeScript qui sopra, i tipi dei parametri a e b sono definiti esplicitamente come numeri. Se viene passata come argomento una stringa, TypeScript emetterà un errore in fase di compilazione, fornendo un feedback tempestivo che permette di individuare eventuali problemi.

Caratteristiche di TypeScript

TypeScript offre numerose e moderne funzionalità di sviluppo web che permettono di superare alcuni dei limiti di JavaScript. Queste caratteristiche migliorano l’esperienza di sviluppo e l’organizzazione del codice. In questo articolo analizzeremo:

1. Tipizzazione statica

TypeScript dispone di un solido sistema di tipizzazione che consente di specificare in fase di compilazione i tipi di variabili e parametri delle funzioni. Questo permette di individuare tempestivamente gli errori legati al tipo, rende il codice più affidabile e meno soggetto a bug.

In JavaScript, invece, le variabili sono tipizzate dinamicamente, il che significa che il tipo può cambiare durante l’esecuzione.

Ad esempio, il codice JavaScript qui sotto mostra la dichiarazione di due variabili tipizzate dinamicamente come numero e stringa:

let num1 = 10; // num1 is dynamically typed as a number
let num2 = "20"; // num2 is dynamically typed as a string

let result = num1 + num2; // No error at compile-time
console.log(result); // Output: "1020"

Questo codice restituirà “1020”, una concatenazione di numero e stringa: non è l’output previsto e ciò può avere un effetto negativo sul codice. Lo svantaggio di JavaScript è che non emette alcun errore. Con TypeScript si può risolvere il problema specificando i tipi di ogni variabile:

let num1: number = 10; // num1 is statically typed as a number
let num2: string = "20"; // num2 is statically typed as a string

let result = num1 + num2; // Error: Type 'string' is not assignable to type 'number'

Nel codice qui sopra, il tentativo di concatenare un numero e una stringa utilizzando l’operatore + dà luogo a un errore in fase di compilazione, perché TypeScript applica un controllo rigoroso.

Questo permette di catturare possibili bug legati al tipo prima dell’esecuzione del codice, permettendo di scrivere codice più solido e privo di errori.

2. Tipizzazione opzionale

TypeScript permette di scegliere se usare o meno la tipizzazione statica. Ciò significa che è possibile decidere di specificare i tipi per le variabili e i parametri delle funzioni o lasciare che TypeScript deduca automaticamente i tipi in base al valore assegnato.

Ad esempio:

let num1: number = 10; // num1 is statically typed as a number
let num2 = "20"; // num2 is dynamically typed as a string

let result = num1 + num2; // Error: Operator '+' cannot be applied to types 'number' and 'string'

In questo codice, il tipo num2 viene dedotto come string in base al valore assegnato, ma se lo si desidera, si può scegliere il tipo.

È anche possibile impostare il tipo su any, che accetta qualsiasi tipo di valore:

let num1: number = 10;
let num2: any = "20";

let result = num1 + num2; // Error: Operator '+' cannot be applied to types 'number' and 'string'

3. Caratteristiche ES6+

TypeScript supporta le moderne funzionalità di JavaScript, comprese quelle introdotte con ECMAScript 6 (ES6) e versioni successive.

Ciò consente di scrivere codice più pulito ed espressivo utilizzando funzioni come le arrow function, la destrutturazione, i template literal e altro ancora, con un type checking aggiuntivo.

Ad esempio:

const greeting = (name: string): string => {
  return `Hello, ${name}!`; // Use of arrow function and template literal
};

console.log(greeting("John")); // Output: Hello, John!

In questo codice, la arrow function e il template literal sono utilizzati perfettamente. Lo stesso vale per tutta la sintassi JavaScript.

4. Organizzazione del codice

In JavaScript, l’organizzazione del codice in file separati e la gestione delle dipendenze possono diventare un problema quando la base di codice cresce. TypeScript supporta moduli e namespace che permettono di organizzare meglio il codice.

I moduli permettono di incapsulare il codice all’interno di file separati, semplificando la gestione e la manutenzione di codebase di grandi dimensioni.

Ecco un esempio:

// greeting.ts:
export function greet(name: string): string { // Export a function from a module
  return `Hello, ${name}!`;
}

// app.ts:
import { greet } from "./greeting"; // Import from a module

console.log(greet("John")); // Output: Hello, John!

Nell’esempio precedente, abbiamo due file separati greeting.ts e app.ts. Il file app.ts importa la funzione greet dal file greeting.ts usando la dichiarazione import. Il file greeting.ts esporta la funzione greet utilizzando la parola chiave export, rendendola accessibile per l’importazione in altri file.

Questo consente di migliorare l’organizzazione del codice e la separazione delle competenze, semplificando la gestione e la manutenzione di grandi basi di codice.

I namespace in TypeScript permettono di raggruppare il codice correlato ed evitare l’inquinamento del namespace globale. Possono essere utilizzati per definire un container per un insieme di classi, interfacce, funzioni o variabili correlate.

Ecco un esempio:

namespace Utilities {
  export function greet(name: string): string {
	return `Hello, ${name}!`;
  }
 
  export function capitalize(str: string): string {
	return str.toUpperCase();
  }
}

console.log(Utilities.greet("John")); // Output: Hello, John!
console.log(Utilities.capitalize("hello")); // Output: HELLO

In questo codice, definiamo un namespace Utilities che contiene due funzioni, greet e capitalize. Possiamo accedere a queste funzioni utilizzando il nome del namespace seguito dal nome della funzione, fornendo un raggruppamento logico per il codice correlato.

5. Caratteristiche della programmazione orientata agli oggetti (OOP)

TypeScript supporta i concetti della OOP come le classi, le interfacce e l’ereditarietà. Ciò permette di creare codice strutturato e ben organizzato.

Ad esempio:

class Person {
  constructor(public name: string) {} // Define a class with a constructor
  greet(): string { // Define a method in a class
	return `Hello, my name is ${this.name}!`;
  }
}

const john = new Person("John"); // Create an instance of the class
console.log(john.greet()); // Output: Hello, my name is John!

6. Type system avanzato

TypeScript offre un type system avanzato che supporta generics, unions, intersections e altro. Queste caratteristiche migliorano le capacità di controllo statico dei tipi e permette di scrivere codice più solido ed espressivo.

Generics: i generics permettono di scrivere codice riutilizzabile che può funzionare con diversi tipi. Sono come dei segnaposto per i tipi che vengono determinati in fase di esecuzione in base ai valori passati a una funzione o a una classe.

Ad esempio, definiamo una funzione generica identity che accetta un parametro di tipo T e restituisce un valore dello stesso tipo T:

function identity(value: T): T {
  return value;
}

let num: number = identity(10); // T is inferred as number
let str: string = identity("hello"); // T is inferred as string

Nell’esempio precedente, il tipo T viene dedotto in base al tipo di valore passato alla funzione. Nel primo utilizzo della funzione identity, T viene dedotto come numero perché viene passato 10 come argomento; nel secondo utilizzo, T viene dedotto come stringa perché viene passato "hello" come argomento.

Unions e intersections: le unions e le intersections si usano per comporre i tipi e creare relazioni più complesse tra tipi.

Le unions permettono di combinare due o più tipi in un unico tipo che può assumere uno qualsiasi dei tipi combinati. Le intersections permettono di combinare due o più tipi in un unico tipo che deve soddisfare tutti i tipi combinati.

Ad esempio, possiamo definire due tipi Employee e Manager, che rappresentano rispettivamente un dipendente e un manager.

type Employee = { name: string, role: string };
type Manager = { name: string, department: string };

Usando i tipi Employee e Manager, possiamo definire un tipo union EmployeeOrManager che può essere un Employee o un Manager.

type EmployeeOrManager = Employee | Manager; // Union type

let person1: EmployeeOrManager = { name: "John", role: "Developer" }; // Can be either Employee or Manager

Nel codice qui sopra, la variabile person1 è di tipo EmployeeOrManager, il che significa che le può essere assegnato un oggetto che soddisfa i tipi Employee o Manager.

Possiamo anche definire un tipo intersection EmployeeOrManager che deve soddisfare entrambi i tipi Employee e Manager.

type EmployeeAndManager = Employee & Manager; // Intersection type

let person2: EmployeeAndManager = { name: "Jane", role: "Manager", department: "HR" }; // Must be both Employee and Manager

Nel codice precedente, la variabile person2 è di tipo EmployeeAndManager, il che significa che deve essere un oggetto che soddisfa entrambi i tipi Employee e Manager.

7. Compatibilità con JavaScript

TypeScript è un superset di JavaScript, il che significa che qualsiasi blocco di codice JavaScript valido è valido anche per TypeScript. Questo permette di integrare TypeScript in progetti JavaScript senza dover riscrivere tutto.

TypeScript si basa su JavaScript, aggiungendo la tipizzazione statica e altre funzionalità, ma consente comunque di utilizzare codice JavaScript.

Ad esempio, un file JavaScript app.js può essere rinominato in app.ts e da qui è possibile iniziare a utilizzare gradualmente le funzionalità di TypeScript senza modificare il codice JavaScript esistente. TypeScript sarà comunque in grado di comprendere e compilare il codice JavaScript come TypeScript valido.

Ecco un esempio di integrazione tra TypeScript e JavaScript:

// app.js - Existing JavaScript code
function greet(name) {
  return "Hello, " + name + "!";
}

console.log(greet("John")); // Output: Hello, John!

Si può rinominare il file JavaScript qui sopra in app.ts e iniziare a utilizzare le funzionalità di TypeScript:

// app.ts - Same JavaScript code as TypeScript
function greet(name: string): string {
  return "Hello, " + name + "!";
}

console.log(greet("John")); // Output: Hello, John!

Nell’esempio precedente, aggiungiamo un’annotazione di tipo al parametro name, specificandola come string, che è opzionale in TypeScript. Il resto del codice rimane uguale a JavaScript. TypeScript è in grado di comprendere il codice JavaScript e di eseguire il type checking per l’annotazione del tipo, cosa che facilita l’adozione graduale di TypeScript in un progetto JavaScript esistente.

Iniziare a sviluppare con TypeScript

TypeScript è un compiler che si può installare in un progetto utilizzando npm. Basta eseguire il comando che segue nella directory del progetto:

npm install -D typescript

Questo comando installerà il compiler nella directory node_modules, che si potrà eseguire con il comando npx tsc.

Il primo passo è inizializzare un progetto node e creare il file package.json utilizzando il seguente comando:

npm init -y

Quindi si potrà installare la dipendenza TypeScript, creare file TypeScript con estensione .ts e scrivere codice TypeScript.

Poi bisognerà compilare il codice in JavaScript utilizzando il compiler TypeScript (tsc). Il comando da eseguire nella directory del progetto sarà:

npx tsc .ts

Questo comando compila il codice TypeScript in JavaScript nel file specificato e genera un file .js con lo stesso nome.

Si potrà quindi eseguire il codice JavaScript compilato nel progetto, proprio come si farebbe con il normale codice JavaScript. Si può utilizzare Node.js per eseguire il codice JavaScript in un ambiente Node.js o includere il file JavaScript compilato in un file HTML ed eseguirlo in un browser.

Lavorare con le interfacce

In TypeScript si usano le interfacce per definire contratti o la forma degli oggetti. Permettono di specificare la struttura o la forma che un oggetto deve rispettare.

Le interfacce definiscono un insieme di proprietà e/o metodi che un oggetto deve avere per essere considerato compatibile con l’interfaccia. Possono inoltre essere utilizzate per fornire annotazioni sul tipo per oggetti, parametri di funzione e valori di ritorno, offrendo un maggiore controllo statico del tipo e suggerimenti per il completamento del codice negli IDE.

Ecco un esempio di interfaccia in TypeScript:

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

In questo esempio, definiamo un’interfaccia Person che specifica tre proprietà: firstName di tipo string, lastName di tipo string e age di tipo number.

Qualsiasi oggetto che abbia queste tre proprietà con i tipi specificati sarà considerato compatibile con l’interfaccia Person. Definiamo ora gli oggetti conformi all’interfaccia Person:

let person1: Person = {
  firstName: "John",
  lastName: "Doe",
  age: 30
};

let person2: Person = {
  firstName: "Jane",
  lastName: "Doe",
  age: 25
};

In questo esempio, abbiamo creato due oggetti person1 e person2 conformi all’interfaccia Person. Entrambi gli oggetti hanno le proprietà richieste firstName, lastName e age con i tipi specificati, quindi sono compatibili con l’interfaccia Person.

Estendere le interfacce

Le interfacce possono essere estese per creare nuove interfacce che ereditano proprietà da quelle esistenti.

Ad esempio:

interface Animal {
  name: string;
  sound: string;
}

interface Dog extends Animal {
  breed: string;
}

let dog: Dog = {
  name: "Buddy",
  sound: "Woof",
  breed: "Labrador"
};

In questo esempio, definiamo l’interfaccia Animal con le proprietà name e sound, e poi definiamo una nuova interfaccia “Dog” che estende l’interfaccia Animal e aggiunge una nuova proprietà breed. L’interfaccia Dog eredita le proprietà dall’interfaccia Animal, quindi qualsiasi oggetto conforme all’interfaccia Dog deve avere anche le proprietà name e sound.

Proprietà opzionali

Le interfacce possono avere anche delle proprietà opzionali, indicate da un ? dopo il nome della proprietà.

Ecco un esempio:

interface Car {
  make: string;
  model: string;
  year?: number;
}

let car1: Car = {
  make: "Toyota",
  model: "Camry"
};

let car2: Car = {
  make: "Honda",
  model: "Accord",
  year: 2020
};

In questo esempio, abbiamo definito un’interfaccia Car con le proprietà make e model e una proprietà opzionale year. La proprietà year non è obbligatoria, quindi gli oggetti conformi all’interfaccia Car possono averla o meno.

Type checking avanzato

TypeScript fornisce anche opzioni avanzate per il type checking nel file tsconfig.json. Queste opzioni possono migliorare le capacità di type checking di un progetto TypeScript e permettere di individuare pssibili errori in fase di compilazione, ottenendo un codice più affidabile.

1. strictNullChecks

Se impostato su true, TypeScript applica controlli null rigorosi, il che significa che le variabili non possono avere un valore null o undefined, a meno che non siano esplicitamente specificate con il tipo di unione di null o undefined.

Ad esempio:

{
  "compilerOptions": {
	"strictNullChecks": true
  }
}

Se si abilita questa opzione, TypeScript catturerà i possibili valori di null o undefined in fase di compilazione, prevenendo errori di runtime causati dall’accesso a proprietà o metodi su variabili null o undefined.

// Example 1: Error - Object is possibly 'null'
let obj1: { prop: string } = null;
console.log(obj1.prop);

// Example 2: Error - Object is possibly 'undefined'
let obj2: { prop: string } = undefined;
console.log(obj2.prop);

2. strictFunctionTypes

Se questa opzione è impostata su true, TypeScript abilita il controllo rigoroso dei tipi delle funzioni, compresa la bivarianza dei parametri di funzione. Questo fa sì che la compatibilità dei tipi degli argomenti delle funzioni sia controllata rigorosamente.

Ad esempio:

{
  "compilerOptions": {
	"strictFunctionTypes": true
  }
}

Abilitando questa opzione, in fase di compilazione TypeScript individuerà le possibili discrepanze tra i tipi di parametri delle funzioni, prevenendo errori di runtime causati dal passaggio di argomenti errati.

// Example: Error - Argument of type 'number' is not assignable to parameter of type 'string'
function greet(name: string) {
  console.log(`Hello, ${name}!`);
}

greet(123);

3. noImplicitThis

Impostando questa opzione su true, TypeScript non consentirà l’utilizzo di this con un tipo implicito any, il che aiuta a individuare errori quando si usa this nei metodi delle classi.

Ad esempio:

{
  "compilerOptions": {
	"noImplicitThis": true
  }
}

Abilitando questa opzione, TypeScript individuerà i possibili errori causati dall’utilizzo di this senza annotazioni di tipo o binding adeguati nei metodi di classe.

// Example: Error - The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'
class MyClass {
  private prop: string;

  constructor(prop: string) {
	this.prop = prop;
  }

  printProp() {
	console.log(this.prop);
  }
}

let obj = new MyClass("Hello");
setTimeout(obj.printProp, 1000); // 'this' context is lost, potential error

4. target

L’opzione target specifica la versione di destinazione di ECMAScript del codice TypeScript, il che vuol dire che stabilisce la versione di JavaScript che il compiler TypeScript deve generare come output.

Ad esempio:

{
  "compilerOptions": {
	"target": "ES2018"
  }
}

Assegnando il valore “ES2018“, TypeScript genererà codice JavaScript conforme allo standard ECMAScript 2018.

Questo può essere utile se si desidera sfruttare le ultime funzionalità e la sintassi più recente di JavaScript, ma sarà necessario anche garantire la retrocompatibilità con gli ambienti JavaScript più vecchi.

5. module

L’opzione module specifica il sistema di moduli da utilizzare nel codice TypeScript. Le opzioni più utilizzate sono “CommonJS“,”AMD“, “ES6“, “ES2015“, ecc. Questo stabilisce il modo in cui i moduli di TypeScript vengono compilati in moduli JavaScript.

Ad esempio:

{
  "compilerOptions": {
	"module": "ES6"
  }
}

Se l’opzione è impostata su “ES6“, TypeScript genererà un codice JavaScript che usa la sintassi dei moduli ECMAScript 6.

Può essere utile se si sta lavorando con un ambiente JavaScript moderno che supporta i moduli ECMAScript 6, per esempio in un’applicazione front-end che utilizza un bundler di moduli come webpack o Rollup.

6. noUnusedLocals e noUnusedParameters

Queste opzioni permettono a TypeScript di catturare rispettivamente le variabili locali e i parametri delle funzioni non utilizzati.

Se impostato su true, TypeScript emetterà errori di compilazione per qualsiasi variabile locale o parametro di funzione dichiarato ma non utilizzato nel codice.

Ad esempio:

{
  "compilerOptions": {
	"noUnusedLocals": true,
	"noUnusedParameters": true
  }
}

Questi sono solo alcuni esempi di opzioni avanzate di type checking che è possibile impostare nel file tsconfig.json di TypeScript. Si legga la documentazione ufficiale per saperne di più.

Best practice e suggerimenti per utilizzare al meglio TypeScript

1. Annotare correttamente i tipi per le variabili, i parametri delle funzioni e i valori restituiti

Uno dei vantaggi principali di TypeScript è la disponibilità di un solido sistema di tipizzazione, che permette di specificare esplicitamente tipi di variabili, parametri di funzione e valori restituiti.

Questo migliora la leggibilità del codice, individua tempestivamente possibili errori legati al tipo e consente di completare in modo intelligente il codice negli IDE.

Ecco un esempio:

// Properly annotating variable types
let age: number = 25;
let name: string = "John";
let isStudent: boolean = false;
let scores: number[] = [98, 76, 89];
let person: { name: string, age: number } = { name: "John", age: 25 };

// Properly annotating function parameter and return types
function greet(name: string): string {
  return "Hello, " + name;
}

function add(a: number, b: number): number {
  return a + b;
}

2. Funzionalità avanzate di TypeScript

TypeScript è dotato di un ricco set di funzionalità avanzate per i tipi, come generics, unions, intersections, conditional types e mapped types. Queste possono aiutarvi a scrivere codice più flessibile e riutilizzabile.

Ecco un esempio:

// Using generics to create a reusable function
function identity(value: T): T {
  return value;
}

let num: number = identity(42); // inferred type: number
let str: string = identity("hello"); // inferred type: string

// Using union types to allow multiple types
function display(value: number | string): void {
  console.log(value);
}

display(42); // valid
display("hello"); // valid
display(true); // error

3. Scrivere codice mantenibile e scalabile con TypeScript

TypeScript permette di scrivere codice mantenibile e scalabile grazie a interfacce, classi e moduli.

Ecco un esempio:

// Using interfaces for defining contracts
interface Person {
  name: string;
  age: number;
}

function greet(person: Person): string {
  return "Hello, " + person.name;
}

let john: Person = { name: "John", age: 25 };
console.log(greet(john)); // "Hello, John"

// Using classes for encapsulation and abstraction
class Animal {
  constructor(private name: string, private species: string) {}

  public makeSound(): void {
	console.log("Animal is making a sound");
  }
}

class Dog extends Animal {
  constructor(name: string, breed: string) {
	super(name, "Dog");
	this.breed = breed;
  }

  public makeSound(): void {
	console.log("Dog is barking");
  }
}

let myDog: Dog = new Dog("Buddy", "Labrador");
myDog.makeSound(); // "Dog is barking"

4. Strumenti e supporto di TypeScript negli IDE

TypeScript dispone di eccellenti strumenti di supporto negli IDE, con funzioni quali l’autocompletamento, la type inference, il refactoring e il controllo degli errori.

Queste funzionalità permettono di aumentare la produttività e individuare possibili errori nelle prime fasi dello sviluppo. Ci sono ottimi IDE compatibili con TypeScript, come Visual Studio Code, e basta installare il plugin TypeScript per avere una migliore esperienza di editing del codice.

Schermata dell’estensione VS Code TypeScript
Estensione VS Code TypeScript

Riepilogo

TypeScript offre un grande ventaglio di potenti funzionalità che possono migliorare notevolmente i progetti di sviluppo web.

Grazie alla tipizzazione statica forte, al sistema di tipi avanzato e alle capacità di programmazione orientata agli oggetti, è uno strumento prezioso per scrivere codice solido, mantenibile e scalabile. Il tooling e il supporto di TypeScript da parte degli IDE migliorano notevolmente l’esperienza di sviluppo.

Se volete esplorare TypeScript e le sue funzionalità, potete farlo oggi stesso grazie all’Hosting di Applicazioni di Kinsta.