Full Stack

Le generics in TypeScript

Autore

Manuel Ricci

Le generics sono uno degli strumenti più potenti e flessibili in TypeScript. Offrono un modo per creare componenti che possono lavorare con una varietà di tipi anziché un singolo tipo. Ciò aggiunge un livello di astrazione e riutilizzabilità nel codice, mantenendo la sicurezza dei tipi.

Applicazioni Pratiche dei Generics

  1. Collezioni tipizzate: Uno degli usi più comuni delle generics è nella creazione di strutture dati come array, liste, o alberi, che possono contenere qualsiasi tipo di dato, garantendo al contempo la sicurezza dei tipi. Ad esempio, un Array<T> può essere un array di numeri, stringhe o qualsiasi altro tipo personalizzato, a seconda di come T viene specificato.

  2. Funzioni Polimorfiche: Le generics permettono di scrivere funzioni che possono operare su diversi tipi di dati. Questo rende il codice più riutilizzabile e flessibile. Ad esempio, una funzione di ordinamento generica può essere utilizzata per ordinare array di numeri, stringhe, o oggetti personalizzati, a condizione che sia fornita una funzione di confronto adeguata.

  3. Tipizzazione di API: In scenari di sviluppo frontend e backend, l'uso delle generics per definire i tipi di risposte e richieste API può aumentare la leggibilità e la manutenibilità del codice. Ciò consente di gestire diverse strutture di dati restituite dalle API in modo più sicuro e coerente.

Costruzione di Tipi e Funzioni Generiche

Definizione di Tipi Generici: Un tipo generico in TypeScript viene definito utilizzando una o più variabili di tipo. Ad esempio, un tipo generico Box<T> potrebbe rappresentare una scatola che può contenere qualsiasi tipo di oggetto. La T qui rappresenta il tipo di oggetto contenuto.

1interface Box<T> {
2    contents: T;
3}

Creazione di Funzioni Generiche: Una funzione generica è definita in modo simile, specificando una variabile di tipo nei suoi parametri. Ad esempio, una funzione per restituire l'elemento di un array potrebbe essere scritta come segue:

1function getFirstElement<T>(arr: T[]): T | undefined {
2    return arr[0];
3}

Questa funzione può ora essere utilizzata con array di qualsiasi tipo, mantenendo la sicurezza dei tipi.

Vincoli sulle generics: TypeScript permette anche di definire vincoli sui Generics, assicurando che i tipi utilizzati soddisfino certe condizioni. Ad esempio, potresti voler limitare un Generic a tipi che hanno una certa proprietà o metodi.

1interface Lengthwise {
2    length: number;
3}
4
5function logLength<T extends Lengthwise>(t: T): void {
6    console.log(t.length);
7}

In questo esempio, logLength accetta un argomento di qualsiasi tipo T, a condizione che T abbia una proprietà length. Questo assicura che la funzione sia utilizzata in modo appropriato e sicuro dal punto di vista dei tipi.

Generics e Programmazione Avanzata

Interfacce Generiche: Le interfacce in TypeScript possono essere generiche, permettendo la definizione di contratti flessibili per una varietà di tipi. Per esempio, un'interfaccia per un servizio di caching potrebbe utilizzare Generics per gestire diversi tipi di dati memorizzati:

1interface CacheElement<T> {
2    get(key: string): T;
3    set(key: string, value: T): void;
4}

Un Cache<number> rappresenterebbe un cache per numeri, mentre un Cache<User> sarebbe un cache per oggetti utente.

Tipi Generici Condizionali: TypeScript supporta tipi condizionali che permettono di creare tipi che dipendono da condizioni. Questo è particolarmente utile per creare funzioni e componenti che si comportano diversamente a seconda del tipo di dati forniti.

1type NumericOrString<T> = T extends number ? 'Numeric' : 'String';

Qui, NumericOrString sarà 'Numeric' se T è un numero, altrimenti sarà 'String'.

Generics Default: TypeScript permette di specificare valori di default per le generics, rendendoli opzionali. Questo è utile quando un tipo generico è comunemente usato con un certo tipo.

1interface APIResponse<T = string> {
2    data: T;
3    status: number;
4}

In questo caso, se il tipo T non è specificato, sarà utilizzato il tipo string per default.

Qualche esempio in più

1function getLastElement<T>(arr: T[]): T | undefined {
2    if (arr.length === 0) return undefined;
3    return arr[arr.length - 1];
4}
5
6// Esempio di utilizzo con un array di numeri
7const numbers = [1, 2, 3, 4, 5];
8const lastNumber = getLastElement(numbers); // lastNumber sarà di tipo 'number'
9
10// Esempio di utilizzo con un array di stringhe
11const strings = ["apple", "banana", "cherry"];
12const lastString = getLastElement(strings); // lastString sarà di tipo 'string'
13
14// Esempio di utilizzo con un array di oggetti
15interface User {
16    id: number;
17    name: string;
18}
19
20const users: User[] = [
21    { id: 1, name: "Alice" },
22    { id: 2, name: "Bob" },
23    { id: 3, name: "Charlie" }
24];
25
26const lastUser = getLastElement(users); // lastUser sarà di tipo 'User'

In questo esempio, la funzione getLastElement<T> è una funzione generica che accetta un array di qualsiasi tipo T. Il tipo T viene quindi utilizzato per indicare che il valore restituito dalla funzione sarà dello stesso tipo degli elementi dell'array.

Grazie all'uso delle generics, la funzione getLastElement è flessibile e riutilizzabile per array di qualsiasi tipo, mantenendo al contempo la sicurezza dei tipi fornita da TypeScript. Questo significa che il compilatore sarà in grado di rilevare errori se si tenta di utilizzare il risultato della funzione in modo incompatibile con il suo tipo.

Perché non usare any al posto delle generics?

1class Queue {
2    private elements: any[] = [];
3
4    enqueue(element: any): void {
5        this.elements.push(element);
6    }
7
8    dequeue(): any {
9        return this.elements.shift();
10    }
11
12    peek(): any {
13        return this.elements[0];
14    }
15
16    isEmpty(): boolean {
17        return this.elements.length === 0;
18    }
19}
20
21// Utilizzo della coda senza specificare il tipo
22const queue = new Queue();
23queue.enqueue(1);
24queue.enqueue("Hello");
25queue.enqueue({ id: 3, description: "Write tests" });
26
27console.log(queue.dequeue()); // Potrebbe restituire un numero, una stringa o un oggetto
28console.log(queue.peek());    // Non si può prevedere il tipo dell'elemento restituito

In questo esempio, la classe Queue utilizza il tipo any per i suoi elementi. Questo significa che la coda può contenere un mix di tipi diversi: numeri, stringhe, oggetti, ecc. Mentre questo offre flessibilità, comporta anche dei rischi:

  1. Perdita della Sicurezza dei Tipi: Non c'è garanzia che gli elementi all'interno della coda siano dello stesso tipo. Questo può portare a errori a runtime se il codice che utilizza la coda si aspetta un certo tipo di elemento.

  2. Manutenzione del Codice: Senza un tipo specifico, è difficile capire come si intende utilizzare la coda guardando il codice. Ciò può rendere il codice più difficile da leggere e mantenere.

  3. Rischio di Errori: Poiché la coda può contenere qualsiasi tipo, è facile inserire accidentalmente un tipo di dato errato, che potrebbe non essere rilevato fino a runtime.

Utilizzando le generics, possiamo evitare questi problemi assicurandoci che tutti gli elementi in una particolare istanza della coda siano dello stesso tipo, migliorando la sicurezza e la leggibilità del codice.

1class Queue<T> {
2    private elements: T[] = [];
3
4    enqueue(element: T): void {
5        this.elements.push(element);
6    }
7
8    dequeue(): T | undefined {
9        return this.elements.shift();
10    }
11
12    peek(): T | undefined {
13        return this.elements[0];
14    }
15
16    isEmpty(): boolean {
17        return this.elements.length === 0;
18    }
19}
20
21// Esempio di utilizzo con numeri
22const numberQueue = new Queue<number>();
23numberQueue.enqueue(1);
24numberQueue.enqueue(2);
25console.log(numberQueue.dequeue()); // Restituirà 1
26console.log(numberQueue.peek());    // Restituirà 2
27
28// Esempio di utilizzo con stringhe
29const stringQueue = new Queue<string>();
30stringQueue.enqueue("Hello");
31stringQueue.enqueue("World");
32console.log(stringQueue.dequeue()); // Restituirà "Hello"
33console.log(stringQueue.peek());    // Restituirà "World"
34
35// Esempio con un tipo oggetto personalizzato
36interface Task {
37    id: number;
38    description: string;
39}
40
41const taskQueue = new Queue<Task>();
42taskQueue.enqueue({ id: 1, description: "Write code" });
43taskQueue.enqueue({ id: 2, description: "Test app" });
44console.log(taskQueue.dequeue()); // Restituirà l'oggetto Task con id 1
45console.log(taskQueue.peek());    // Restituirà l'oggetto Task con id 2

In questo esempio, la classe Queue<T> è una classe generica che può essere usata per creare code di qualsiasi tipo di elemento. Abbiamo definito metodi per aggiungere (enqueue), rimuovere (dequeue) e visualizzare l'elemento in cima alla coda (peek), oltre a un metodo per controllare se la coda è vuota (isEmpty).

Grazie all'utilizzo delle generics, possiamo creare istanze della coda per gestire specifici tipi di dati (numeri, stringhe, oggetti, ecc.), mantenendo la sicurezza e la coerenza dei tipi in tutto il codice. Questo approccio migliora la leggibilità, la manutenibilità e riduce la possibilità di errori legati al tipo di dato.

Conclusioni

L'uso delle generics in TypeScript è fondamentale per creare applicazioni robuste e flessibili. Consentono di scrivere codice più astratto e riutilizzabile, mantenendo la sicurezza dei tipi. Che si tratti di creare collezioni tipizzate, funzioni polimorfiche o gestire tipi di dati complessi, i Generics offrono gli strumenti per gestire la varietà e la complessità dei dati in modo efficiente e sicuro.

In conclusione, le generics non sono solo una funzionalità avanzata per gli sviluppatori TypeScript; sono un elemento cruciale per scrivere codice pulito, mantenibile e di alta qualità in ambienti di sviluppo moderni. Con la comprensione e l'applicazione efficace dei Generics, gli sviluppatori possono sfruttare appieno il potere e la flessibilità di TypeScript.

Caricamento...

Diventiamo amici di penna? ✒️

Iscriviti alla newsletter per ricevere una mail ogni paio di settimane con le ultime novità, gli ultimi approfondimenti e gli ultimi corsi gratuiti puubblicati. Ogni tanto potrei scrivere qualcosa di commerciale, ma solo se mi autorizzi, altrimenti non ti disturberò oltre.

Se non ti piace la newsletter ti ricordo la community su Discord, dove puoi chiedere aiuto, fare domande e condividere le tue esperienze (ma soprattutto scambiare due meme con me). Ti aspetto!

Ho in previsione di mandarti una newsletter ogni due settimane e una commerciale quando capita.