
GUIDE PER ASPIRANTI PROGRAMMATORI
Manipolazione di tipi in Typescript
A questo punto, abbiamo tutti gli strumenti per usare il type system come un vero e proprio linguaggio di programmazione a compile time. In questa sezione vedremo come creare tipi a partire da altri tipi, sfruttando vincoli e condizioni per creare complesse strutture a partire da pochi tipi di riferimento o persino da dati JavaScript…


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
A questo punto, abbiamo tutti gli strumenti per usare il type system come un vero e proprio linguaggio di programmazione a compile time. In questa sezione vedremo come creare tipi a partire da altri tipi, sfruttando vincoli e condizioni per creare complesse strutture a partire da pochi tipi di riferimento o persino da dati JavaScript non tipizzati.
Iniziamo subito estendendo quello che abbiamo visto sugli object types.
Ti avvertiamo: sarà una carrellata! In questa sezione della guida Typescript in italiano andiamo semplicemente a dimostrare le infinite possibilità offerte dal type system, senza individuare esempi e casi d’uso approfonditi per ognuna di esse, perché sarebbe pesantissimo e poco interessante; sarà comunque molto utile avere una panoramica completa di come si può assumere un controllo molto capillare dei tipi per una vasta varietà di situazioni specifiche. Cominciamo!
Usando il punto di domanda ? possiamo indicare su un’interfaccia o un object literal che una certa proprietà può anche non essere presente. Implicitamente, tale proprietà diventerà potenzialmente undefined:
interface OptionalProp { prop?: string; } const a: OptionalProp = {}; const b: OptionalProp = { prop: "hello" }; const c: OptionalProp = { prop: undefined };
Indicare una proprietà come opzionale non equivale a indicarla come | undefined: in quel caso, saremmo comunque obbligati a inizializzarla come in c, mentre l’inizializzazione di a non sarebbe valida.
Possiamo anche annotare alcune proprietà di un oggetto come di sola lettura con la parola chiave readonly:
interface ReadonlyProp { readonly prop: string; } const a: ReadonlyProp = { prop: "hello" }; a.prop = "hi"; // Cannot assign to 'prop' because it is a read-only property.
Se non conosciamo tutte le proprietà di un oggetto ma ne conosciamo la forma, possiamo indicarlo con una firma d’indice (in inglese: index signature):
interface IndexSignature { [key: string]: number; } const a: IndexSignature = { someProp: 21, someOtherProp: 42 };
Le firme d’indice possono essere applicate insieme alla definizione di altre proprietà, a patto che queste rispettino tale firma:
interface IndexSignature { [key: string]: number; prop1: number; prop2: string; // Property 'prop2' of type 'string' is not assignable to 'string' index type 'number'. }
Partendo da un tipo oggetto possiamo ricavare il tipo delle sue proprietà usando la notazione oggetto[“chiave”]:
interface SomeProps { num: number; str: string; bool: boolean; } type Num = SomeProps["num"]; // Num = number type Str = SomeProps["str"]; // Str = string type Bool = SomeProps["bool"] // Bool = boolean
Possiamo fare la stessa cosa con le tuple, indicando l’indice numerico al posto della chiave:
type Tuple = [number, string, boolean]; type Num = Tuple[0]; // Num = number type Str = Tuple[1]; // Str = string type Bool = Tuple[2] // Bool = boolean
Con l’operatore keyof otteniamo l’unione delle chiavi di un object type:
interface SomeProps { num: number; str: string; bool: boolean; } type Key = keyof SomeProps; // Key = "num" | "str" | "bool"
Possiamo fare l’operazione inversa, indicando un oggetto che ha come chiavi le stringhe di un union type; tali tipi sono detti mapped types:
type Keys = "prop1" | "prop2" | "prop3"; type KeyNumber = { [K in Keys]: number; } const a: KeyNumber = { prop1: 1, prop2: 2, prop3: 3 };
Possiamo applicare delle condizioni sui tipi usando la parola chiave extends insieme all’operatore ternario:
type Keys = "num" | "str" | "bool"; type KeyValue = { [K in Keys]: K extends "num" ? number : K extends "str" ? string : boolean; } const a: KeyValue = { num: 1, str: "hello", bool: false };
Un mapped type non può essere definito con un’interfaccia, ma solo come type alias. Qui K funziona come un parametro di tipo che itera tra le possibili opzioni dell’unione.
Tramite i mapped types possiamo applicare i modificatori delle proprietà appena visti e persino applicare condizioni per ricavare tipi oggetto da altri tipi oggetto:
interface Model { readonly id: number; createdAt: Date; name: string; description?: string; } // rende readonly tutte le proprietà type ReadonlyModel = { readonly [K in keyof Model]: Model[K]; }; // rende opzionali tutte le proprietà type AllOptionalModel = { [K in keyof Model]?: Model[K]; }; // rende non-readonly tutte le proprietà type AllMutableModel = { -readonly [K in keyof Model]: Model[K]; }; // rende non-opzionali tutte le proprietà type AllRequiredModel = { [K in keyof Model]-?: Model[K]; }; // rende booleane tutte le proprietà type AllBooleanModel = { [K in keyof Model]: boolean; }; // rimuove tutte le proprietà che non hanno tipo 'number' type OnlyNumberPropsModel = { [K in keyof Model as Model[K] extends number ? K : never]: Model[K]; };
I tipi condizionali possono essere utilizzati anche in combinazione con i generics:
type NumberOrNever<T> = T extends number ? T : never; type A = NumberOrNever<12>; // A = 12 type B = NumberOrNever<string>; // B = never type C = NumberOrNever<"hello">; // C = never type D = NumberOrNever<number | string>; // D = number
Per concludere, vediamo come possiamo ricavare tipi dai dati JavaScript.
La parola chiave typeof espone il tipo di una variabile:
const two = 2; type Two = typeof two; // Two = 2 let num = 1; type Num = typeof num; // Num = number const user = { name: "John Doe", email: "[email protected]", phone: "345 678 9012" }; type User = typeof user; // User = { // name: string; // email: string; // phone: string; // }
Questo può essere molto comodo quando si vuole definire un certo oggetto e il suo tipo contemporaneamente.
Va notato che quando una variabile o proprietà è modificabile, TypeScript le assegna un tipo non letterale, mentre se è costante le assegna il tipo letterale corrispondente al suo valore.
Se volessimo trattare un intero oggetto o array come immutabile per ricavarne esattamente il tipo letterale, possiamo usare as const; vediamo come cambiano le definizioni precedenti:
const two = 2; // as const qui non serve type Two = typeof two; // Two = 2 let num = 1 as const; type Num = typeof num; // Num = 1 const user = { name: "John Doe", email: "[email protected]", phone: "345 678 9012" } as const; type User = typeof user; // User = { // readonly name: "John Doe"; // readonly email: "[email protected]"; // readonly phone: "345 678 9012"; // }
Le notazioni viste fin qui ci permettono di manipolare con chirurgica precisione ciò che vogliamo prelevare da un tipo o persino da un dato preesistente e come vogliamo modificarlo; tuttavia, non è verosimile pensare di usarle frequentemente visto che sono tante e piuttosto complesse e, dunque, non favoriscono particolarmente la leggibilità e la comprensione dei nostri tipi. La cosa più verosimile è che tali notazioni siano utilizzate in modo isolato per definire dei tipi generici utili per applicare le modifiche viste ai tipi esistenti nel nostro dominio.
Nella sezione seguente vedremo che la maggior parte di questi generici utili ci viene fornita nativamente da TypeScript.
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.