
GUIDE PER ASPIRANTI PROGRAMMATORI
Tipi algebrici in Typescript
Nelle sezioni iniziali di questa guida abbiamo parlato del fatto che TypeScript offre un type system statico, e questo comporta che una variabile non possa cambiare tipo durante il suo ciclo di vita. Eppure, sappiamo che il nostro codice TypeScript è destinato a diventare JavaScript, che invece ha un type system dinamico. Come abbiamo visto…


Vuoi avviare una nuova carriera o fare un upgrade?
Trova il corso Digital & Tech più adatto a te nel nostro catalogo!
- Tipi primitivi in Typescript
- Tipi letterali in Typescript
- Null e undefined in Typescript
- Tipi enumerativi in Typescript
- Oggetti in Typescript
- Array e tuple in Typescript
- Funzioni in Typescript
- Guardie di tipo in Typescript
- Any vs unknown in Typescript
- Tipi algebrici in Typescript
- Parametri di tipo in Typescript
- Manipolazione di tipi in Typescript
- Tipi utility in Typescript
Nelle sezioni iniziali di questa guida abbiamo parlato del fatto che TypeScript offre un type system statico, e questo comporta che una variabile non possa cambiare tipo durante il suo ciclo di vita. Eppure, sappiamo che il nostro codice TypeScript è destinato a diventare JavaScript, che invece ha un type system dinamico. Come abbiamo visto per gli array, un compromesso tra il rigore di TypeScript e la flessibilità di JavaScript consiste nell’indicare una combinazione dei possibili tipi che un dato potrebbe assumere:
type Item = number | string | null; const items: Item[] = [null, 1, "hello", 42];
È finalmente giunto momento di sviscerare il principio che sta alla base di tipi come Item e dell’operatore |, ma anche del suo fratello &. Il motivo per cui affrontiamo questo argomento solo ora è che, un po’ come l’algebra, i tipi algebrici sono un costrutto tanto utile quanto avanzato; i tipi algebrici sono, generalmente, utilizzati in linguaggi funzionali come Haskell e F# che, per quanto stiano guadagnando popolarità, sono pur sempre dei linguaggi di programmazione meno diffusi dei linguaggi C-like come il linguaggio Java, C# e, per l’appunto, il linguaggio Javascript.
In TypeScript è posssibile definire un tipo come combinazione di due o più altri tipi, secondo le operazioni insiemistiche di intersezione o unione. I tipi così definiti sono detti tipi algebrici (in inglese: algebraic data types).
Tipo unione in Typescript (union type)
Con l’operatore | rappresentiamo un tipo unione (in inglese: union type), cioè un tipo che rappresenta la possibilità di appartenere a uno dei tipi elencati:
type NumberOrString = number | string; type Bit = 0 | 1; type Bool = true | false; // Bool = boolean let a: NumberOrString = 2; a = "hello"; a = new Date(); // Type 'Date' is not assignable to type 'NumberOrString'. let bit: Bit = 0; bit = 1; bit = 3; // Type '3' is not assignable to type 'Bit'. let bool: Bool = false; bool = true;
Con l’unione di tipi possiamo sfruttare la flessibilità di JavaScript mantenendo un certo rigore sulla definizione dei possibili tipi che la nostra variabile può assumere: quando andremo in lettura su una variabile che ha come tipo uno union type, TypeScript ci obbligherà a fare un narrowing o un’asserzione esplicita per identificare il sottotipo concreto che la variabile assume in quel punto del codice.
Dal tipo Bool definito nell’esempio possiamo notare anche che TypeScript sia perfettamente consapevole che la sua definizione coincide con il tipo primitivo boolean, che, infatti, non è altro che un tipo che può assumere uno di due valori letterali. Un’altra categoria di tipi che possono solo assumere un elenco finito di valori letterali sono gli enum, che abbiamo visto in precedenza; infatti, è molto comune usare | con numeri o stringhe per creare una versione semplificata e leggera di enum.
Vediamolo in un esempio con tanto di switch/case:
// possiamo dichiarare un'unione anche su più righe type Status = | "idle" | "loading" | "error" | "success"; function logStatusMessage(status: Status): void { switch (status) { case "idle": console.log("nothing happened :|"); return; case "loading": console.log("please wait..."); return; case "error": console.log("ops, something went wrong :("); return; case "success": console.log("we did it! :D"); return; default: // questo caso è teoricamente impossibile throw new Error("Unreachable code"); } }
Qui abbiamo fatto la stessa identica cosa che avremmo fatto con un enum, ma a tutti gli effetti ci siamo risparmiati l’implementazione dell’enum a runtime e abbiamo usato delle semplici stringhe come tag per i possibili casi. Nelle sezioni finali di questa guida ci spingeremo ancora oltre, associando per ogni caso un payload specifico, una tecnica molto comune per la gestione degli stati (in inglese: state management).
Tipi intersezione in Typescript (Intersection Types)
Vediamo, ora, la controparte degli union types, cioè i tipi intersezione; anche in questo caso andiamo a combinare più tipi per formarne uno nuovo, ma questa volta il tipo risultante andrà a identificare i dati che appartengono a entrambi i tipi di partenza:
type Entity = { id: number }; type User = { name: string; email: string; }; type UserEntity = Entity & User; const user: User = { name: "John Doe", email: "[email protected]" }; const userEntity: UserEntity = { id: 1, ...user };
In questo caso, poiché userEntity ha tutte le proprietà richieste da User e da Entity, soddisfa il tipo UserEntity.
Notiamo che, a differenza degli esempi fatti per gli union types, qui non abbiamo usato primitivi ma oggetti, e il motivo è presto rivelato: l’intersezione tra tipi individua quei dati che hanno l’unione delle proprietà di tali tipi; è controintuitivo, ma ha perfettamente senso da una prospettiva insiemistica, ed è altrettanto vero l’opposto per i tipi unione.
Tuttavia, non essendoci proprietà compatibili per essere unite tra i tipi primitivi, l’intersezione tra questi è sempre il tipo never. Vediamolo nell’esempio, nel paragrafo a seguire.
Tipo never in Typescript
// A = B = C = never type A = number & string; type B = string & boolean; type C = number & boolean;
Queste intersezioni non sono molto utili dato che nessun valore (nemmeno null o undefined) può essere assegnato a never. Intuitivamente, risulta chiaro come un oggetto possa soddisfare più tipi se include tutte le proprietà richieste da ognuno di essi, ma in nessun caso una variabile potrà mai (per l’appunto, never) essere contemporaneamente un numero e una stringa, nemmeno nell’iperflessibile type system di JavaScript.
Ovviamente la stessa cosa avviene per intersezioni di literal types:
// A = B = C = never type A = "hello" & "hi"; type B = 1 & 4; type C = true & 42;
A questo punto, si potrebbe pensare che questo valga anche per l’intersezione tra primitivi e oggetti, ma c’è il tranello, che vediamo nel paragrafo successivo.
Branded types in Typescript
type A = string & Date; type B = number & string[]; type C = 3 & { name: string };
Notiamo che nessuno di questi tipi viene ridotto a never, nonostante sia chiaramente impossibile creare variabili di questi tipi. Il motivo per questa strana eccezione è che ci consente di creare dei tipi speciali che rappresentano un payload di tipo primitivo in una struttura unica riconoscibile dal type checker, senza però incapsulare realmente questo in un oggetto. Questi tipi speciali vengono chiamati branded types (tipi marchiati) e abilitano delle particolari tecniche che ovviamente vedremo più avanti, non prima di aver finito il nostro tour del type system.
CONTENUTI GRATUITI IN EVIDENZA
Guide per aspiranti programmatori 👨🏻🚀
Vuoi muovere i primi passi nel Digital e Tech? Abbiamo preparato alcune guide per aiutarti a orientarti negli ambiti più richiesti oggi.