Nel frenetico panorama digitale di oggi, JavaScript è diventato il linguaggio di riferimento per la creazione di applicazioni web dinamiche. Tuttavia, la digitazione dinamica di JavaScript può talvolta portare a errori impercettibili, cosa che li rende difficili da individuare nelle prime fasi del processo di sviluppo.
È qui che entra in gioco TypeScript, una tecnologia che rivoluziona il modo in cui scriviamo il codice JavaScript.
In questo articolo ci immergiamo nel mondo di TypeScript e ne esploriamo le caratteristiche, i vantaggi e le best practice. Scoprirete come TypeScript affronta i limiti di JavaScript e sblocca la potenza della tipizzazione statica nella costruzione di applicazioni web robuste e scalabili.
Cominciamo!
Cos’È TypeScript?
TypeScript è un superset di JavaScript che aggiunge tipizzazione statica opzionale 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 è risultato essere 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 durante lo sviluppo.
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
Il codice precedente 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, ma concatena i valori come stringhe, provocando un comportamento inatteso.
Con TypeScript è stata introdotta la tipizzazione statica opzionale, che consente a chi sviluppa di specificare i tipi di variabili, i parametri delle funzioni e i valori di ritorno, individuando gli errori legati al tipo durante lo sviluppo.
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 una stringa come argomento, TypeScript emetterà un errore in fase di compilazione, fornendo un feedback tempestivo per individuare possibili 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. Comprendono:
1. Tipizzazione statica
TypeScript dispone di un solido sistema di tipizzazione che consente di specificare i tipi di variabili e parametri delle funzioni in fase di compilazione. 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 vostro 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 aiuta a catturare possibili bug legati al tipo prima dell’esecuzione del codice, permettendo di scrivere codice più robusto 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 in ECMAScript 6 (ES6) e versioni successive.
Ciò consente a chi sviluppa 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 per organizzare meglio il codice.
I moduli permettono di incapsulare il codice all’interno di file separati, rendendo più semplice 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 una migliore organizzazione del codice e la separazione delle competenze, facilitando 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à, permettendo di creare un 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 ancora. Queste caratteristiche migliorano le capacità di controllo statico dei tipi di TypeScript, 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 tra tipi più complesse.
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 di 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 codice JavaScript valido è valido anche per TypeScript. Questo permette di integrare TypeScript in progetti JavaScript senza dover riscrivere tutto il codice.
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 fornire il type checking per l’annotazione del tipo, cosa che facilita l’adozione graduale di TypeScript in un progetto JavaScript esistente.
Iniziare 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 utilizzando il seguente comando per creare un file package.json:
npm init -y
Poi 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 gli oggetti, i parametri delle funzioni e i valori di ritorno, offrendo un migliore 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, creiamo 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, definiamo 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ù solido e 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, permettendo di prevenire gli 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, che garantisce che la compatibilità dei tipi degli argomenti delle funzioni sia controllata rigorosamente.
Ad esempio:
{
"compilerOptions": {
"strictFunctionTypes": true
}
}
Abilitando questa opzione, TypeScript individuerà le possibili discrepanze tra i tipi di parametri delle funzioni in fase di compilazione, permettendo di prevenire gli 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 nell’uso di 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 vuold 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 determina 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.
Questo 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 un completamento intelligente del 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. Le funzionalità avanzate di TypeScript
TypeScript è dotato di un ricco set di funzioni avanzate per i tipi, come i generics, le unions, le intersections, i conditional types e i 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 incoraggia la scrittura di codice mantenibile e scalabile grazie a funzioni quali 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. Sfruttare gli strumenti e il supporto IDE di TypeScript
TypeScript dispone di strumenti e supporto di IDE eccellenti, con funzioni quali l’autocompletamento, la type inference, il refactoring e il controllo degli errori.
Queste funzionalità permettono di migliorare 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.

Riepilogo
TypeScript offre un grande ventaglio di potenti funzionalità che possono migliorare notevolmente i progetti di sviluppo web.
La forte tipizzazione statica, il sistema di tipi avanzato e le capacità di programmazione orientata agli oggetti lo rendono uno strumento prezioso per scrivere codice solido, mantenibile e scalabile. Il tooling e il supporto IDE di TypeScript offrono inoltre un’esperienza di sviluppo fluida.
Se volete esplorare TypeScript e le sue funzionalità, potete farlo oggi stesso grazie all’Hosting di Applicazioni di Kinsta.