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
- Elementi della Tupla etichettati
- Proprietà delle Classi determinate dai Costruttori
- Operatori di assegnazione composti con cortocircuito
unknown
per i parametri del bloccocatch
- JSX Factories personalizzate
- Velocità migliorata per
build
con--noEmitOnError
--incremental
con--noEmit
/** @deprecated */
Supportato- Modifiche non retrocompatibili
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)