Perché any fa perdere il tipo che hai passato?
Tipizza un parametro come any e TypeScript smette di controllare tutto ciò che esce dalla funzione:
| |
Tu sai di aver passato un array di numeri. Il compilatore no, perché any gli dice di smettere di guardare. n torna come any, quindi chiamare un metodo di stringa su un numero compila senza problemi e lancia TypeError: n.toUpperCase is not a function quando viene eseguito.
Un generic nomina il tipo invece di scartarlo:
| |
T è un parametro di tipo, un segnaposto che viene riempito a ogni chiamata. firstElement([1, 2, 3]) riempie T con number, il tipo di ritorno diventa number, e la chiamata sbagliata a .toUpperCase() fallisce nel tuo editor invece che in produzione. Scrivi la funzione una volta sola e ogni tipo di elemento mantiene il suo controllo.
Il resto di questa guida spiega come usarli senza rendere la firma più difficile da leggere del bug che prevengono.
Come fa TypeScript a inferire il parametro di tipo?
Non hai scritto firstElement<number>([1, 2, 3]) qui sopra, e raramente ne hai bisogno. TypeScript confronta l’argomento con la forma dichiarata del parametro (T[]) e risolve T. È la stessa inferenza che tipizza const x = 5 come number, applicata ai parametri di tipo.
Senza nulla da cui inferire, T ricade su unknown:
| |
Quando la chiamata non ha un valore da cui inferire, passa tu stesso l’argomento di tipo tra parentesi angolari.
L’inferenza può anche andare storta quando due argomenti contribuiscono allo stesso T ma non sono compatibili tra loro. TypeScript sceglie un candidato e segnala l’altro argomento come il problema, il che può produrre un errore che nomina un tipo che non hai mai scritto:
| |
Quando una chiamata generica dà un errore confuso o un tipo inaspettato, passa il mouse sulla chiamata nel tuo editor e leggi cosa è stato inferito per T. Di solito questo spiega il problema più in fretta della lettura del messaggio.
Come vincolare un generic con extends e keyof?
Un T non vincolato può essere qualsiasi cosa, quindi dentro la funzione puoi fare solo ciò che ogni tipo supporta, che è quasi niente oltre a passare il valore in giro. La maggior parte delle funzioni generiche reali deve presupporre qualcosa su T, e extends è il modo per dichiararlo:
| |
K extends keyof T limita K ai nomi delle proprietà dell’oggetto che passi, e T[K] legge il tipo di quella proprietà. L’elenco delle chiavi valide viene dall’oggetto al punto della chiamata, non da un’unione scritta a mano. Un errore di battitura come "pay" diventa un errore di compilazione invece di un undefined silenzioso.
Il vincolo può anche essere una forma semplice:
| |
Stringhe e array hanno entrambi una length, quindi passano entrambi. Un numero no, e la chiamata viene respinta prima che il corpo della funzione venga mai eseguito. Dato che il tipo di ritorno è T e non { length: number }, logLength("hello") ti restituisce comunque una string.
Come funzionano interfacce e classi generiche?
Un’interfaccia generica è una forma con un parametro di tipo:
| |
Una classe generica condivide il suo parametro di tipo tra tutti i campi e metodi:
| |
new Stack<number>() e new Stack<string>() sono entrambi completamente tipizzati, e la classe esiste una sola volta. Lo stesso schema ricorre ovunque usi la dependency injection: scrivi un Repository<T> una volta, poi inietti Repository<User> e Repository<Order> dove servono invece di mantenere una classe quasi identica per ogni entità.
Più parametri di tipo funzionano allo stesso modo, e ognuno può avere il suo vincolo:
| |
Due parametri sono anche il modo in cui un tipo Result<T, E> combina un valore di successo e un tipo di errore. Se stai decidendo se le funzioni devono lanciare eccezioni o restituire i fallimenti come dati, eccezioni vs valori di errore approfondisce quella scelta.
Come funzionano i parametri di tipo di default?
Un parametro di tipo può avere un default, come un parametro di funzione con un valore predefinito:
| |
L’inferenza vince ancora quando c’è un argomento da cui inferire, come mostra raw. Il default si applica quando nulla riempie T, per esempio quando scrivi ApiResponse come annotazione di tipo senza argomento di tipo. Questo rende i default utili nei wrapper come i tipi di risposta e i payload degli eventi, dove molti punti di chiamata non si preoccupano del tipo esatto dei dati. Usa come default unknown, non any: obbliga comunque a fare un controllo prima che qualcuno legga data.
Quando usare un union type invece di un generic?
Fatti una domanda prima di aggiungere <T>: il tipo di ritorno dipende da cosa è stato passato? Se la funzione restituisce sempre lo stesso tipo indipendentemente dall’input, un’unione basta e un generic non aggiunge niente:
| |
firstElement, getProperty e Stack<T> sono diversi. Ognuno restituisce qualcosa il cui tipo è deciso da ciò che è entrato, e quel legame tra input e output è ciò che un generic ti dà. Se riesci a scrivere la funzione con un’unione senza perdere niente, usa l’unione.
Accumulare parametri di tipo perché una funzione potrebbe essere generica costa leggibilità e non porta nessun vantaggio. Un helper con tre parametri di tipo che viene chiamato solo con string e number è più difficile da leggere di due overload. Aggiungi <T> quando un chiamante reale ha bisogno che un tipo fluisca attraverso, e rimuovilo quando ogni punto di chiamata fissa lo stesso tipo concreto.
Perché non puoi usare T a runtime?
I parametri di tipo vengono eliminati in fase di compilazione. Il JavaScript che viene eseguito non contiene nessun T, quindi non puoi costruirne uno, controllare instanceof T, o fare branching su di esso:
| |
Se una funzione deve costruire un T o ispezionarlo a runtime, passa un valore reale come argomento: un costruttore, uno schema, o un tag di tipo. I generics sono un contratto a tempo di compilazione, non una protezione a runtime, quindi i dati che arrivano dall’esterno del tuo programma (una risposta di un’API, JSON.parse) vanno comunque validati anche quando sono tipizzati come ApiResponse<User>.
Per vedere ogni errore di questa guida con le tue mani, installa TypeScript con npm, Yarn o pnpm, a seconda di quale usa il tuo progetto, incolla gli esempi in un file .ts ed esegui npx tsc --noEmit --strict. Poi apri l’any più usato nel tuo codice e verifica se un <T> farebbe fluire il tipo.