DEV Community

Matteo Spreafico
Matteo Spreafico

Posted on • Originally published at matteospreafico.com on

Annunciato TypeScript 4.0 RC

Un paio di giorni Microsoft ha annunciato la Release Candidate della nuova major version di Typescript, la 4.0.

Solitamente un cambio di major version è accompagnato da sostanziali novità e modifiche non retrocompatibili, ma il team di Typescript ha fatto il possibile per minimizzare le incompatibilità, così come avevano fatto per la Typescript 3.9.

Per iniziare subito ad usare la RC basta installarla tramite npm

npm install typescript@rc

Ma vediamo l'elenco delle novità:

Tipi variadici di Tuple

Consideriamo una funzione in JavaScript chiamata concat che prende due array o tuple e li concatena insieme in un nuovo array.

function concat(arr1, arr2) {
    return [...arr1, ...arr2];
}

Inoltre consideriamo tail, che accetta un array o tupla e restituisce tutti gli elementi tranne il primo.

function tail(arg) {
    const [_, ...result] = arg;
    return result
}

Come possiamo tipizzare queste funzioni in TypeScript?

Per concat, l'unica cosa possibile nelle vecchie versioni del linguaggio era provare a scrivere alcuni overloads.

function concat<>(arr1: [], arr2: []): [A];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)

... sono sette overloads per quando il secondo array è sempre vuoto. Aggiungiamone altri per quando arr2 ha un argomento.

function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];

Mi sembra ovvio che non ha senso questa soluzione. Sfortunatamente lo stesso tipo di problema si incontra con funzioni simili a tail.

Questo è anche chiamato “morte per mille overloads” e non risolvere il problema in maniera generica è limitato al numero di overload che vogliamo effettivamente scrivere. Se volessimo una definizione generica dovremmo scrivere qualcosa simile a:

function concat<T, U>(arr1: T[], arr2, U[]): Array<T | U>;

Ma questo non ci dice nulla sulla lunghezza degli dei parametri in ingresso o sull'ordine dei sui elementi quando usiamo le tuple.

TypeScript 4.0 contiene 2 modifiche sostanziali, per rendere questi tipi possibili.

La prima modifica consiste nel fatto che lo spread nei tipi delle tuple può essere un generic. Questo significa che possiamo rappresentare operazione di ordine superiore nelle tuple e negli array anche quando non sappiamo l'effettivo tipo su cui stiamo operando. Quando uno generic spread è istanziato (or sostituito con un tipo concreto) in questi tipi di tuple, generano dei nuovi gruppo di tipi di array e tuple.

Per esempio, questo significa che possiamo assegnare un tipo alle funzioni come tail, senza il problema della “morte per mille overloads”.

function tail<T extends any[]>(arr: readonly [any, ...T]) {
    const [_ignored, ...rest] = arr;
    return rest;
}

const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];

// type [2, 3, 4]
const r1 = tail(myTuple);

// type [2, 3, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);

La seconda modifica è che l'elemento di spread può essere in qualunque posizione all'interno di una tupla, non solo alla fine!

type Strings = [string, string];
type Numbers = [number, number];

// [string, string, number, number]
type StrStrNumNum = [...Strings, ...Numbers];

In precedenza, TypeScript avrebbe dato il seguente errore.

A rest element must be last in a tuple type.

Ma ora il linguaggio può gestire gli spreads in ogni posizione.

Quando facciamo lo spread di un tipo senza una lunghezza nota, il tipo risultante diventa illimitato a sua volta e tutti gli elementi seguenti sono raccolti nel risultante tipo dell'elemento restante.

type Strings = [string, string];
type Numbers = number[]

// [string, string, ...Array<number | boolean>]
type Unbounded = [...Strings, ...Numbers, boolean];

Combinando entrambi questi comportamenti, possiamo scrivere un solo tipo per concat:

type Arr = readonly any[];

function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
    return [...arr1, ...arr2];
}

Nonostante la definizione sia leggermente lunga, è solo una ed è stata scritta una sola volta e fornisce un comportamento predicibile per gli array e le tuple.

Questa funzionalità è molto interessante, ma ci sono anche degli scenari più complessi. Ad esempio, consideriamo una funzione che applica parzialmente gli argomenti e la chiamiamo partialCall. partialCall prende una funziona insieme agli argomenti che quella funzione di aspetta e poi ritorna una nuova funzione che prende tutti gli altri argomenti che quella funzione si aspetta e la chiama.

function partialCall(f, ...headArgs) {
    return (...tailArgs) => f(...headArgs, ...tailArgs)
}

TypeScript 4.0 migliora il processo di inferenza di tipo per i restanti parametri e gli elementi della tupla restante in modo da generare un tipo che "semplicemente funziona".

type Arr = readonly unknown[];

function partialCall<T extends Arr, U extends Arr, R>(f: (...args: [...T, ...U]) => R, ...headArgs: T) {
    return (...b: U) => f(...headArgs, ...b)
}

In questo caso, partialCall capisce quali parametri può o non può inizialmente ricevere e ritorna funzioni che di conseguenza accettano o rifiutano i rimanenti.

const foo = (x: string, y: number, z: boolean) => {}

// Questo non funziona perchè stiamo passando il tipo sbagliato per 'x'.
const f1 = partialCall(foo, 100);
// ~~~
// error! Argument of type 'number' is not assignable to parameter of type 'string'.

// Questo non funziona perchè stiamo passando troppi argomenti
const f2 = partialCall(foo, "hello", 100, true, "oops")
// ~~~~~~
// error! Expected 4 arguments, but got 5.

// Questo funziona! Il suo tipo è '(y: number, z: boolean) => void'
const f3 = partialCall(foo, "hello");

// Cosa possiamo fare con f3?

f3(123, true); // funziona!

f3();
// error! Expected 2 arguments, but got 0.

f3(123, "hello");
// ~~~~~~~
// error! Argument of type '"hello"' is not assignable to parameter of type 'boolean'.

I tipi variadici di Tuple permettono dei nuovi scenari, specialmente nella composizione di funzioni.

Elementi della Tupla etichettati

Migliorare l'utilizzo dei tipi tupla è importante perché ci permette di avere la validazione del tipo anche in comuni costrutti Javascript come quando si manipola la lista degli argomenti per passarli ad altre funzioni.

Ad esempio, questa funziona usa una tupla per gli argomenti restanti

function foo(...args: [string, number]): void {
    // ...
}

e dovrebbe essere del tutto equivalente a

function foo(arg0: string, arg1: number): void {
    // ...
}

per chiunque chiami foo

foo("ciao", 42); // funziona

foo("ciao", 42, true); // errore
foo("ciao"); // errore

C'è però una differenza evidente: la leggibilità! Nel primo esempio, non abbiamo un nome dei parametri per il primo ed il secondo elemento. Anche non c'è nessuna differenza nel controllo dei tipi, la mancanza di etichette nella tupla le può rendere più difficili da usare e comprendere.

Per questo motivo, in TypeScript 4.0, i tipi tupla possono fornire le etichette.

type Intervallo = [inizio: number, fine: number];

Per rendere ancora più evidente il legame tra liste di parametri e tipi tupla, abbiamo reso la sintassi per i restanti elementi e gli elementi facoltativi simile a quella delle liste di parametri.

type Foo = [primo: number, secondo?: string, ...resto: any[]];

Quando si aggiunge un'etichetta ad un elemento della tupla, tutti gli altri elementi della tupla devono essere etichettati

type Bar = [primo: string, number];
// ~~~~~~
// error! Tuple members must all have names or all not have names.

Vale la pena notare che le etichette non ci obbligano a dare un nome differente alle variabili quando destrutturiamo. Servono unicamente come documentazione.

function foo(x: [primo: string, secondo: number]) {
// ...

// nota: non dobbiamo chiamare queste 'primo' e 'secondo'
let [a, b] = x;

// ...
}

Per saperne di più potete vedere la pull request per i labeled tuple elements.

Proprietà delle Classi determinate dai Costruttori

Typescript 4.0 ora può usare l'analisi del flusso di controllo per determinare il tipo delle proprietà nelle classi quando noImplicitAny è abilitato.

class Quadrato {
    // Precedentemente: any implicito
    // Ora: determinato `number`
    area;
    lunghezzaLato;

    constructor(lunghezzaLato: number) {
        this.lunghezzaLato = lunghezzaLato;
        this.area = lunghezzaLato ** 2;
    }
}

Quando non tutti i percorsi del costruttore assegnano un valore, la proprietà è considerata potenzialmente undefined.

class Quadrato {
    lunghezzaLato;

    constructor(lunghezzaLato: number) {
        if (Math.random()) {
            this.lunghezzaLato = lunghezzaLato;
        }
    }

    get area() {
        return this.lunghezzaLato ** 2;
        // ~~~~~~~~~~~~~~~
        // error! Object is possibly 'undefined'.
    }
}

Operatori di assegnazione composti con cortocircuito

JavaScript, come molti altri linguaggi, supporto un gruppo di operatori chiamati operatori di assegnazione composti. Questi operatori, applicano un'operazione a due argomenti e poi assegnano il risultato a sinistra. Li avrete probabilmente già visti:

// Somma
// a = a + b
a += b;

// Sottrazione
// a = a - b
a -= b;

// Moltiplicazione
// a = a * b
a *= b;

// Divisione
// a = a / b
a /= b;

// Elevamento a potenza
// a = a ** b
a **= b;

// Bit Shift a sinisstra
// a = a << b
a <<= b;

Molti operatori in JavaScript hanno il corrispondente operatore di assegnazione composto! Ma ci sono tre importanti eccezioni: and logico (&&), or logico (||), and coalescenza nulla (??).

Per questo motivo, TypeScript 4.0 supporta la promettente proposta di includere questi nuovi operatori: &&=, ||=, and ??=.

Questi operatori sono utili in ogni caso un utente voglia scrivere codice come il seguente:

a = a && b;
a = a || b;
a = a ?? b;

Questi sono alcuni esempi di utilizzo dove si utilizza il lazy initialization.

let valori: string[];

// Prima
(valori ?? (valori = [])).push("ciao");

// Dopo
(valori ??= []).push("ciao");

Nel raro caso in cui tu usi getters o setters con effetti collaterali, ricorda che questi operatori eseguono l'assegnamento solo se necessario. In questo senso l'assegnamento è cortocircuitato, che rappresenta una differenza rispetto agli altri operatori di assegnazione composti.

a ||= b;

// equivalente a

a || (a = b);

Per altre informazioni, potete vedere la pull request.

unknown per i parametri del catch

Da sempre i parametri dei blocchi catch hanno sempre avuto il tipo any. Questo vuol dire che TypeScript ti lascia fare qualunque cosa con loro.

try {
    // ...
}
catch (x) {
    // x ha come tipo 'any' - buon divertimento!
    console.log(x.message);
    console.log(x.toUpperCase());
    x++;
    x.yadda.yadda.yadda();
}

Dato che i parametri del blocco catch hanno il tipo any in automatico, la mancanza del controllo di tipo non ci aiuta ad evitare di introdurre altri errori nel blocco di codice dedicato alla gestione degli errori!

Per questo motivo TypeScript 4.0 ora ti lascia indicare il tipo di ogni variabile del blocco catch come unknown. unknown è più sicuro di any perché ti ricorda che devi controllare il tipo prima di eseguire ogni operazione sui nostri valori.

try {
    // ...
}
catch (e: unknown) {
    // errore!
    // La proprietà 'toUpperCase' non esiste nel tipo 'unknown'.
    console.log(e.toUpperCase());

    if (typeof e === "string") {
        // funziona!
        // Abbiamo verificaro che 'e' ha il tipo 'string'.
        console.log(e.toUpperCase());
    }
}

Il tipo automatico dei parametri dei blocchi catch non cambia, ma potrebbe essere aggiunta una nuova opzione --strict in futuro per abilitare questo comportamento. Nel frattempo, dovrebbe esser possibile scrivere una regola di validazione per richiedere che i parametri dei blocchi catch abbiano il tipo annotato esplicitamente a : any o : unknown.

JSX Factories personalizzate

Quando si usa JSX, un fragment è un tipo di elementi JSX che ti permette restituire diversi elementi figlio. Quando fragment è stato implementato per la prima volta in TypeScript, non era chiaro come le altre librerie lo avrebbero utilizzato.

In TypeScript 4.0, gli utenti possono personalizzare la fragment factory attraverso la nuova opzione jsxFragmentFactory.

Ad esempio, il seguente tsconfig.json dice a TypeScript di trasformare JSX in un modo compatibile con React, ma sostituisce ogni chiamata a React.createElement con h, e usa Fragment invece di React.Fragment.

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "jsx": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}

Nel caso in cui ti serva avere una diversa JSX factory per ogni file, puoi usare il commento /** @jsxFrag */. Ad esempio…

// Nota: per usare questi commenti, devono essere scritti
// usando lo stile JSDoc su più linee.
/** @jsx h */
/** @jsxFrag Fragment */

import { h, Fragment } from "preact";

let stuff = <>
    <div>Hello</div>
</>;

… e verrà trasformato in questo JavaScript…

// Nota: per usare questi commenti, devono essere scritti
// usando lo stile JSDoc su più linee.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = h(Fragment, null,
    h("div", null, "Hello"));

Velocità migliorata per build con --noEmitOnError

Precedentemente, la compilazione di programma che precedentemente aveva degli errori usando --incremental sarebbe risultata estremamente lenta usando il parametro --noEmitOnError. Questo perché nessuna delle informazioni dell'ultima compilazione sarebbero state salvate nel .tsbuildinfo file per via del parametro --noEmitOnError.

TypeScript 4.0 modifica questo comportamento ottenendo un grande miglioramento delle prestazioni in questi scenari, e di conseguenza migliora gli scenari con --build (che implica sia --incremental che --noEmitOnError).

Per altri dettagli, potete far riferimento alla pull request.

--incremental con --noEmit

TypeScript 4.0 permette di usare il parametro --noEmit quando si usa --incremental. Questo non era precedentemente permesso poiché --incremental richiede la creazione del .tsbuildinfo file; ma avere delle build più veloci è molto importante ed è stato abilitato per tutti gli utenti.

Al solito, potete vedere l'implementazione della nella pull request.

/** @deprecated */ Supportato

TypeScript ora riconosce quando una dichiarazione è stata decorata con il commneto JSDoc /** @deprecated *. L'informazione è passata alla lista dei suggerimenti per l'autocompletamento in modo che che l'editor possa gestirla opportunamente. In VS Code ad esempio i valori deprecati sono mostrati col testo sbarrato, come questo.

Modifiche non Retrocompatibili

Modifiche a lib.d.ts

Le dichiarazioni in lib.d.ts sono cambiate, più precisamente i tipi per il DOM sono cambiati. La modifica più rilevante è l'eliminazione di document.origin che funzionava solo in vecchie versioni di IE e Safari MDN raccomanda di passare a self.origin.

Proprietà che ridefiniscono le funzione di accesso (ed il contrario) è un Errore

Precedentemente, era un errore solo quando una proprietà ridefiniva una funzione di accesso o il contrario, quando veniva usato useDefineForClassFields; ora invece TypeScript genererà sempre un errore.

class Base {
    get foo() {
        return 100;
    }
    set foo() {
        // ...
    }
}

class Derived extends Base {
    foo = 10;
// ~~~
// errore!
// 'foo' è definito come una funzione di accesso nella classe 'Base',
// ma è ridefinito nella classe 'Derived' come proprietà.
}

class Base {
    prop = 10;
}

class Derived extends Base {
    get prop() {
    // ~~~~
    // errore!
    // 'prop' è definito come una proprietà nella Classe 'Base',
    // ma è ridefinito in 'Derived' come una funziona di accesso.
        return 100;
    }
}

Gli operandi per delete devono essere facoltativi.

Quando si usa l'operatore delete in strictNullChecks, l'operando deve ora essere any, unknown, never, o essere facoltativo (ovvero contenere il tipo undefined). Altrimenti l'uso dell'operatore delete è un errore.

interface Cosa {
    prop: string;
}

function f(x: Cosa) {
    delete x.prop;
    // ~~~~~~
    // errore! L'operatando dell'operatore 'delete' deve essere facoltativo.
}

L'uso del Node Factory di TypeScript è deprecato

Attualmente TypeScript fornisce una gruppo di funzioni “factory” per produrre AST Nodes; ma ora TypeScript 4.0 offre una nuova node factory API. Di conseguenza con TypeScript 4.0 abbiamo preso la decisione di deprecare le vecchie funzioni in favore delle nuove.

La versione originale di questo annuncio è disponibile sul sito Microsoft

Top comments (0)