TypeScript 5.0 è stato rilasciato ufficialmente il 16 marzo 2023 ed è ora disponibile al pubblico. Questa versione introduce molte nuove funzionalità con l’obiettivo di rendere TypeScript più snello, semplice e veloce.

Questa nuova versione modernizza i decorator per la personalizzazione delle classi, e dunque permette di personalizzare le classi e i loro membri in modo riutilizzabile. Sviluppatrici e sviluppatori possono ora aggiungere un modifier const alla dichiarazione di un parametro di tipo, facendo sì che le inferenze di tipo const siano quelle predefinite. La nuova versione trasforma inoltre tutti gli enum in union enum, semplificando la struttura del codice e velocizzando l’esperienza in TypeScript.

In questo articolo vedremo le modifiche introdotte in TypeScript 5.0, dando uno sguardo approfondito alle sue nuove caratteristiche e funzionalità.

Come Iniziare con TypeScript 5.0

TypeScript è un compiler ufficiale che potete installare nel vostro progetto utilizzando npm. Se volete iniziare a usare TypeScript 5.0, potete eseguire il seguente comando nella directory del progetto:

npm install -D typescript

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

Trovate le istruzioni per usare la nuova versione di TypeScript in Visual Studio Code in questa documentazione.

Cosa c’è di Nuovo in TypeScript 5.0?

In questo articolo esploriamo i 5 principali aggiornamenti introdotti in TypeScript. Queste caratteristiche includono:

Decorator Più Moderni

I decorator sono presenti in TypeScript da un po’ di tempo con un flag sperimentale, ma la nuova versione li aggiorna con la proposta di ECMAScript, che è ora in fase 3, cioè la fase in cui viene aggiunta a TypeScript.

I decorator sono un modo per personalizzare il comportamento delle classi e dei loro membri in modo riutilizzabile. Per esempio, se avete una classe che ha due metodi, greet e getAge:

class Person {
	name: string;
	age: number;
	constructor(name: string, age: number) {
    	this.name = name;
    	this.age = age;
	}

	greet() {
    	console.log(`Hello, my name is ${this.name}.`);
	}

	getAge() {
    	console.log(`I am ${this.age} years old.`);
	}
}

const p = new Person('Ron', 30);
p.greet();
p.getAge();

Nei casi d’uso reali, questa classe dovrebbe avere metodi più complicati, che gestiscono alcune logiche asincrone e hanno effetti collaterali, ecc., in cui inserire alcune chiamate a console.log per facilitare il debug dei metodi.

class Person {
	name: string;
	age: number;
	constructor(name: string, age: number) {
    	this.name = name;
    	this.age = age;
	}

	greet() {
    	console.log('LOG: Method Execution Starts.');
    	console.log(`Hello, my name is ${this.name}.`);
    	console.log('LOG: Method Execution Ends.');
	}

	getAge() {
    	console.log('LOG: Method Execution Starts.');
    	console.log(`I am ${this.age} years old.`);
    	console.log('Method Execution Ends.');
	}
}

const p = new Person('Ron', 30);
p.greet();
p.getAge();

Si tratta di uno schema che ricorre spesso, e avere una soluzione da applicare a ogni metodo sarebbe decisamente comodo.

È qui che entrano in gioco i decorator. Possiamo definire una funzione chiamata debugMethod che si presenta come segue:

function debugMethod(originalMethod: any, context: any) {
	function replacementMethod(this: any, ...args: any[]) {
    	console.log('Method Execution Starts.');
    	const result = originalMethod.call(this, ...args);
    	console.log('Method Execution Ends.');
    	return result;
	}
	return replacementMethod;
}

Nel codice precedente, debugMethod prende il metodo originale (originalMethod) e restituisce una funzione che fa quanto segue:

  1. Registra il messaggio “Method Execution Starts” (“Inizia l’esecuzione del metodo”).
  2. Passa il metodo originale e tutti i suoi argomenti (compreso questo).
  3. Invia il messaggio “Method Execution Ends” (“L’esecuzione del metodo è terminata”).
  4. Restituisce ciò che il metodo originale ha restituito.

Utilizzando i decorator, potete applicare debugMethod ai vostri metodi come mostrato nel codice seguente:

class Person {
	name: string;
	age: number;
	constructor(name: string, age: number) {
    	this.name = name;
    	this.age = age;
	}
	@debugMethod
	greet() {
    	console.log(`Hello, my name is ${this.name}.`);
	}
	@debugMethod
	getAge() {
    	console.log(`I am ${this.age} years old.`);
	}
}
const p = new Person('Ron', 30);
p.greet();
p.getAge();

Il risultato sarà il seguente:

LOG: Entering method.
Hello, my name is Ron.
LOG: Exiting method.
LOG: Entering method.
I am 30 years old.
LOG: Exiting method.

Quando si definisce la funzione decorator (debugMethod), viene passato un secondo parametro chiamato context (è l’oggetto contesto, che contiene alcune informazioni utili su come è stato dichiarato il metodo decorato e anche il nome del metodo). Potete aggiornare il vostro debugMethod per ottenere il nome del metodo dall’oggetto context:

function debugMethod(
	originalMethod: any,
	context: ClassMethodDecoratorContext
) {
	const methodName = String(context.name);
	function replacementMethod(this: any, ...args: any[]) {
    	console.log(`'${methodName}' Execution Starts.`);
    	const result = originalMethod.call(this, ...args);
    	console.log(`'${methodName}' Execution Ends.`);
    	return result;
	}
	return replacementMethod;
}

Quando eseguite il vostro codice, l’output riporterà il nome di ogni metodo decorato con il decorator debugMethod:

'greet' Execution Starts.
Hello, my name is Ron.
'greet' Execution Ends.
'getAge' Execution Starts.
I am 30 years old.
'getAge' Execution Ends.

Le cose che potete fare con i decorator sono molte di più. Per maggiori informazioni su come usare i decorator in TypeScript, consultate la richiesta di pull originale.

Introduzione dei Parametri Const Type

Questo è un altro grande rilascio che vi offre un nuovo strumento con i generici per migliorare l’inferenza quando chiamate le funzioni. Per impostazione predefinita, quando dichiarate dei valori con const, TypeScript ne deduce il tipo e non i valori letterali:

// Inferred type: string[]
const names = ['John', 'Jake', 'Jack'];

Finora, per ottenere l’inferenza desiderata, dovevate usare l’asserzione const aggiungendo “as const”:

// Inferred type: readonly ["John", "Jake", "Jack"]
const names = ['John', 'Jake', 'Jack'] as const;

Quando chiamate delle funzioni, la situazione è simile. Nel codice qui sotto, il tipo dedotto per countries è string[]:

type HasCountries = { countries: readonly string[] };
function getCountriesExactly(arg: T): T['countries'] {
	return arg.countries;
}

// Inferred type: string[]
const countries = getCountriesExactly({ countries: ['USA', 'Canada', 'India'] });

Potreste desiderare un tipo più specifico; prima, un modo per risolvere il problema era quello di aggiungere l’asserzione as const:

// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] } as const);

Questo può essere difficile da ricordare e da implementare. Tuttavia, TypeScript 5.0 introduce una nuova funzione che consente di aggiungere un modificatore const alla dichiarazione di un parametro di tipo, che applicherà automaticamente un’inferenza di tipo const come impostazione predefinita.

type HasCountries = { countries: readonly string[] };
function getNamesExactly(arg: T): T['countries'] {
	return arg.countries;
}

// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] });

L’uso dei parametri type const consente a chi sviluppa di esprimere più chiaramente le intenzioni nel proprio codice. Se una variabile è destinata a rimanere costante e a non cambiare mai, l’uso di un parametro const assicura che non possa essere modificata accidentalmente.

Potete consultare la richiesta di pull originale per maggiori informazioni sul funzionamento del parametro di tipo const in TypeScript.

Miglioramenti agli Enum

Gli enum in TypeScript sono un potente costrutto che permette di definire un insieme di costanti denominate. In TypeScript 5.0 sono stati apportati dei miglioramenti agli enum per renderli ancora più flessibili e utili.

Per esempio, se il seguente enum viene passato in una funzione:

enum Color {
	Red,
	Green,
	Blue,
}

function getColorName(colorLevel: Color) {
	return colorLevel;
}

console.log(getColorName(1));

Prima dell’introduzione di TypeScript 5.0, potevate passare un numero di livello sbagliato e non si verificava alcun errore. Ma con l’introduzione di TypeScript 5.0, verrà immediatamente comunicato un errore.

Inoltre, la nuova versione trasforma tutti gli enum in union enum creando un tipo unico per ogni membro calcolato. Questo miglioramento consente di restringere tutti gli enum e di fare riferimento ai loro membri come tipi:

enum Color {
	Red,
	Purple,
	Orange,
	Green,
	Blue,
	Black,
	White,
}

type PrimaryColor = Color.Red | Color.Green | Color.Blue;

function isPrimaryColor(c: Color): c is PrimaryColor {
	return c === Color.Red || c === Color.Green || c === Color.Blue;
}

console.log(isPrimaryColor(Color.White)); // Outputs: false
console.log(isPrimaryColor(Color.Red)); // Outputs: true

Miglioramenti delle Prestazioni di TypeScript 5.0

TypeScript 5.0 include numerosi cambiamenti significativi nella struttura del codice, nelle strutture dati e nelle estensioni algoritmiche. Questo ha contribuito a migliorare l’intera esperienza di TypeScript, dall’installazione all’esecuzione, rendendola più veloce ed efficiente.

Per esempio, la differenza tra le dimensioni del pacchetto di TypeScript 5.0 e 4.9 è davvero notevole.

TypeScript è stato recentemente migrato dagli spazi dei nomi ai moduli, consentendogli di sfruttare i moderni strumenti di compilazione in grado di eseguire ottimizzazioni come lo scope hoisting. Inoltre, la rimozione di alcuni codici deprecati ha permesso di ridurre di circa 26,4 MB la dimensione del pacchetto di TypeScript 4.9, che era di 63,8 MB.

Grafico a barre che confronta la dimensione del pacchetto TypeScript tra la versione 4.9 e la 5.0: la versione 4.9 pesa 63.8 MB, mentre la 5.0 pesa 37.4 MB
Dimensioni del pacchetto TypeScript

Ecco altri interessanti traguardi raggiunti in termini di velocità e dimensioni tra TypeScript 5.0 e 4.9:

Scenario Tempo o dimensioni rispetto a TS 4.9
Build time di material-ui 90%
Startup time del Compiler TypeScript 89%
Build time di Playwright 88%
Self-build time del compiler TypeScript 87%
Build time di Outlook Web 82%
Build time di VS Code 80%
Dimensione del pacchetto typescript npm 59%

Risoluzione Bundler per una Migliore Risoluzione dei Moduli

Quando scrivete una dichiarazione di importazione in TypeScript, il compiler deve sapere a cosa si riferisce l’importazione. Lo fa utilizzando la risoluzione dei moduli. Per esempio, quando scrivete import { a } from "moduleA", il compiler deve conoscere la definizione di a in moduleA per verificarne l’utilizzo.

In TypeScript 4.7 sono state aggiunte due nuove opzioni per le impostazioni di --module e moduleResolution: node16 e nodenext.

Lo scopo di queste opzioni era quello di rappresentare in modo più accurato le regole di ricerca dei moduli ECMAScript in Node.js. Tuttavia, questa modalità presenta diverse restrizioni che non vengono applicate da altri strumenti.

Per esempio, in un modulo ECMAScript in Node.js, qualsiasi importazione relativa deve includere un’estensione di file per funzionare correttamente:

import * as utils from "./utils"; // Wrong

import * as utils from "./utils.mjs"; // Correct

TypeScript ha introdotto una nuova strategia chiamata “moduleResolution bundler”. Questa strategia può essere implementata aggiungendo il seguente codice nella sezione “compilerOptions” del vostro file di configurazione di TypeScript:

{
	"compilerOptions": {
    	"target": "esnext",
    	"moduleResolution": "bundler"
	}
}

Questa nuova strategia è adatta a chi usa i moderni bundler come Vite, esbuild, swc, Webpack, Parcel e altri che si servono di una strategia di ricerca ibrida.

Potete consultare la richiesta di pull originale e la sua implementazione per maggiori informazioni sul funzionamento del bundler moduleResolution in TypeScript.

Deprecazioni

TypeScript 5.0 include alcune deprecazioni, tra cui requisiti di runtime, modifiche a lib.d.ts e modifiche alle API.

  1. Requisiti di runtime: TypeScript ora si rivolge a ECMAScript 2018 e il pacchetto prevede un motore minimo di 12.20. Pertanto, gli utenti di Node.js devono avere una versione minima di 12.20 o successiva per utilizzare TypeScript 5.0.
  2. Modifiche a lib.d.ts: Sono state apportate alcune modifiche al modo in cui vengono generati i tipi per il DOM, che possono influire sul codice esistente. In particolare, alcune proprietà sono state convertite da tipi numerici a tipi letterali numerici e le proprietà e i metodi per la gestione degli eventi di taglia, copia e incolla sono stati spostati tra le interfacce.
  3. Modifiche alle API: Sono state rimosse alcune interfacce non necessarie e sono stati apportati alcuni miglioramenti alla correttezza. TypeScript 5.0 è passato anche ai moduli.

TypeScript 5.0 ha deprecato alcune impostazioni e i loro valori corrispondenti, tra cui target: ES3, out, noImplicitUseStrict, keyofStringsOnly, suppressExcessPropertyErrors, suppressImplicitAnyIndexErrors, noStrictGenericChecks, charset, importsNotUsedAsValues, e preserveValueImports, così come prepend nei riferimenti al progetto.

Anche se queste configurazioni rimarranno valide fino a TypeScript 5.5, verrà emesso un avviso per gli utenti che le usano ancora.

Riepilogo

In questo articolo abbiamo visto alcune delle principali caratteristiche e miglioramenti apportati da TypeScript 5.0, come i miglioramenti agli enum, alla risoluzione dei bundler e ai parametri di tipo const, oltre ai miglioramenti alla velocità e alle dimensioni.

Se state pensando a TypeScript per i vostri prossimi progetti, provate gratuitamente l’Hosting di Applicazioni di Kinsta.

Ora tocca a voi! Quali sono le caratteristiche o i miglioramenti che trovate più interessanti in TypeScript 5.0? Ce ne sono di significative che potrebbero esserci sfuggite? Fatecelo sapere nei commenti.

Joel Olawanle Kinsta

Joel is a Frontend developer working at Kinsta as a Technical Editor. He is a passionate teacher with love for open source and has written over 200 technical articles majorly around JavaScript and it's frameworks.