Una guida completa ai potenti Tipi Mappati e Tipi Condizionali di TypeScript, con esempi pratici e casi d'uso avanzati per creare applicazioni robuste e type-safe.
Padroneggiare i Tipi Mappati e i Tipi Condizionali di TypeScript
TypeScript, un superset di JavaScript, offre funzionalità potenti per creare applicazioni robuste e manutenibili. Tra queste funzionalità, i Tipi Mappati (Mapped Types) e i Tipi Condizionali (Conditional Types) si distinguono come strumenti essenziali per la manipolazione avanzata dei tipi. Questa guida fornisce una panoramica completa di questi concetti, esplorandone la sintassi, le applicazioni pratiche e i casi d'uso avanzati. Che siate sviluppatori TypeScript esperti o alle prime armi, questo articolo vi fornirà le conoscenze per sfruttare efficacemente queste funzionalità.
Cosa sono i Tipi Mappati?
I Tipi Mappati consentono di creare nuovi tipi trasformando quelli esistenti. Iterano sulle proprietà di un tipo esistente e applicano una trasformazione a ciascuna proprietà. Questo è particolarmente utile per creare variazioni di tipi esistenti, come rendere tutte le proprietà opzionali o di sola lettura.
Sintassi di Base
La sintassi di un Tipo Mappato è la seguente:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: Il tipo di input su cui si desidera mappare.K in keyof T
: Itera su ogni chiave nel tipo di inputT
.keyof T
crea un'unione di tutti i nomi delle proprietà inT
, eK
rappresenta ogni singola chiave durante l'iterazione.Transformation
: La trasformazione che si desidera applicare a ciascuna proprietà. Potrebbe trattarsi dell'aggiunta di un modificatore (comereadonly
o?
), della modifica del tipo o di qualcos'altro.
Esempi Pratici
Rendere le Proprietà di Sola Lettura
Supponiamo di avere un'interfaccia che rappresenta un profilo utente:
interface UserProfile {
name: string;
age: number;
email: string;
}
È possibile creare un nuovo tipo in cui tutte le proprietà sono di sola lettura:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
Ora, ReadOnlyUserProfile
avrà le stesse proprietà di UserProfile
, ma saranno tutte di sola lettura.
Rendere le Proprietà Opzionali
Allo stesso modo, è possibile rendere tutte le proprietà opzionali:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
OptionalUserProfile
avrà tutte le proprietà di UserProfile
, ma ciascuna sarà opzionale.
Modificare i Tipi delle Proprietà
È anche possibile modificare il tipo di ciascuna proprietà. Ad esempio, si possono trasformare tutte le proprietà in stringhe:
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
In questo caso, tutte le proprietà in StringifiedUserProfile
saranno di tipo string
.
Cosa sono i Tipi Condizionali?
I Tipi Condizionali consentono di definire tipi che dipendono da una condizione. Forniscono un modo per esprimere relazioni tra tipi basate sul fatto che un tipo soddisfi o meno un particolare vincolo. È simile a un operatore ternario in JavaScript, ma per i tipi.
Sintassi di Base
La sintassi di un Tipo Condizionale è la seguente:
T extends U ? X : Y
T
: Il tipo che viene controllato.U
: Il tipo che viene esteso daT
(la condizione).X
: Il tipo da restituire seT
estendeU
(la condizione è vera).Y
: Il tipo da restituire seT
non estendeU
(la condizione è falsa).
Esempi Pratici
Determinare se un Tipo è una Stringa
Creiamo un tipo che restituisce string
se il tipo di input è una stringa, e number
altrimenti:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
Estrarre un Tipo da un'Unione
È possibile utilizzare i tipi condizionali per estrarre un tipo specifico da un'unione di tipi. Ad esempio, per estrarre i tipi non nullabili:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
Qui, se T
è null
o undefined
, il tipo diventa never
, che viene poi filtrato dalla semplificazione dei tipi unione di TypeScript.
Inferire i Tipi
I tipi condizionali possono anche essere usati per inferire tipi usando la parola chiave infer
. Ciò consente di estrarre un tipo da una struttura di tipi più complessa.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type Result5 = ReturnType<typeof myFunction>; // string
In questo esempio, ReturnType
estrae il tipo di ritorno di una funzione. Controlla se T
è una funzione che accetta qualsiasi argomento e restituisce un tipo R
. In tal caso, restituisce R
; altrimenti, restituisce any
.
Combinare Tipi Mappati e Tipi Condizionali
La vera potenza dei Tipi Mappati e dei Tipi Condizionali deriva dalla loro combinazione. Ciò consente di creare trasformazioni di tipo altamente flessibili ed espressive.
Esempio: Deep Readonly
Un caso d'uso comune è creare un tipo che renda di sola lettura tutte le proprietà di un oggetto, comprese quelle annidate. Ciò può essere ottenuto utilizzando un tipo condizionale ricorsivo.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
Qui, DeepReadonly
applica ricorsivamente il modificatore readonly
a tutte le proprietà e alle loro proprietà annidate. Se una proprietà è un oggetto, chiama ricorsivamente DeepReadonly
su quell'oggetto. Altrimenti, applica semplicemente il modificatore readonly
alla proprietà.
Esempio: Filtrare le Proprietà per Tipo
Supponiamo di voler creare un tipo che includa solo proprietà di un tipo specifico. È possibile combinare Tipi Mappati e Tipi Condizionali per ottenere questo risultato.
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type StringProperties = FilterByType<Person, string>; // { name: string; }
type NonStringProperties = Omit<Person, keyof StringProperties>;
In questo esempio, FilterByType
itera sulle proprietà di T
e controlla se il tipo di ciascuna proprietà estende U
. In caso affermativo, include la proprietà nel tipo risultante; altrimenti, la esclude mappando la chiave a never
. Notare l'uso di "as" per rimappare le chiavi. Utilizziamo quindi `Omit` e `keyof StringProperties` per rimuovere le proprietà di tipo stringa dall'interfaccia originale.
Casi d'Uso Avanzati e Pattern
Oltre agli esempi di base, i Tipi Mappati e i Tipi Condizionali possono essere utilizzati in scenari più avanzati per creare applicazioni altamente personalizzabili e type-safe.
Tipi Condizionali Distributivi
I tipi condizionali sono distributivi quando il tipo controllato è un tipo unione. Ciò significa che la condizione viene applicata individualmente a ciascun membro dell'unione e i risultati vengono quindi combinati in un nuovo tipo unione.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
In questo esempio, ToArray
viene applicato individualmente a ciascun membro dell'unione string | number
, risultando in string[] | number[]
. Se la condizione non fosse distributiva, il risultato sarebbe stato (string | number)[]
.
Utilizzo dei Tipi di Utilità
TypeScript fornisce diversi tipi di utilità integrati che sfruttano i Tipi Mappati e i Tipi Condizionali. Questi tipi di utilità possono essere utilizzati come mattoni per trasformazioni di tipo più complesse.
Partial<T>
: Rende tutte le proprietà diT
opzionali.Required<T>
: Rende tutte le proprietà diT
obbligatorie.Readonly<T>
: Rende tutte le proprietà diT
di sola lettura.Pick<T, K>
: Seleziona un insieme di proprietàK
daT
.Omit<T, K>
: Rimuove un insieme di proprietàK
daT
.Record<K, T>
: Costruisce un tipo con un insieme di proprietàK
di tipoT
.Exclude<T, U>
: Esclude daT
tutti i tipi che sono assegnabili aU
.Extract<T, U>
: Estrae daT
tutti i tipi che sono assegnabili aU
.NonNullable<T>
: Escludenull
eundefined
daT
.Parameters<T>
: Ottiene i parametri di un tipo funzioneT
.ReturnType<T>
: Ottiene il tipo di ritorno di un tipo funzioneT
.InstanceType<T>
: Ottiene il tipo di istanza di un tipo funzione costruttoreT
.
Questi tipi di utilità sono strumenti potenti che possono semplificare manipolazioni di tipo complesse. Ad esempio, è possibile combinare Pick
e Partial
per creare un tipo che rende opzionali solo determinate proprietà:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type OptionalDescriptionProduct = Optional<Product, "description">;
In questo esempio, OptionalDescriptionProduct
ha tutte le proprietà di Product
, ma la proprietà description
è opzionale.
Utilizzo dei Tipi Template Literal
I Tipi Template Literal consentono di creare tipi basati su letterali di stringa. Possono essere utilizzati in combinazione con i Tipi Mappati e i Tipi Condizionali per creare trasformazioni di tipo dinamiche ed espressive. Ad esempio, è possibile creare un tipo che aggiunge un prefisso a tutti i nomi delle proprietà con una stringa specifica:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Settings {
apiUrl: string;
timeout: number;
}
type PrefixedSettings = Prefix<Settings, "data_">;
In questo esempio, PrefixedSettings
avrà le proprietà data_apiUrl
e data_timeout
.
Migliori Pratiche e Considerazioni
- Mantenere la Semplicità: Sebbene i Tipi Mappati e i Tipi Condizionali siano potenti, possono anche rendere il codice più complesso. Cercate di mantenere le trasformazioni di tipo il più semplici possibile.
- Usare i Tipi di Utilità: Sfruttate i tipi di utilità integrati di TypeScript quando possibile. Sono ben testati e possono semplificare il vostro codice.
- Documentare i Tipi: Documentate chiaramente le vostre trasformazioni di tipo, specialmente se sono complesse. Questo aiuterà altri sviluppatori a comprendere il vostro codice.
- Testare i Tipi: Utilizzate il controllo dei tipi di TypeScript per assicurarvi che le vostre trasformazioni di tipo funzionino come previsto. Potete scrivere unit test per verificare il comportamento dei vostri tipi.
- Considerare le Prestazioni: Trasformazioni di tipo complesse possono influire sulle prestazioni del compilatore TypeScript. Siate consapevoli della complessità dei vostri tipi ed evitate calcoli non necessari.
Conclusione
I Tipi Mappati e i Tipi Condizionali sono funzionalità potenti in TypeScript che consentono di creare trasformazioni di tipo altamente flessibili ed espressive. Padroneggiando questi concetti, è possibile migliorare la sicurezza dei tipi, la manutenibilità e la qualità complessiva delle vostre applicazioni TypeScript. Dalle semplici trasformazioni come rendere le proprietà opzionali o di sola lettura, a complesse trasformazioni ricorsive e logica condizionale, queste funzionalità forniscono gli strumenti necessari per costruire applicazioni robuste e scalabili. Continuate a esplorare e sperimentare con queste funzionalità per sbloccare il loro pieno potenziale e diventare sviluppatori TypeScript più competenti.
Mentre proseguite il vostro viaggio con TypeScript, ricordate di sfruttare la ricchezza di risorse disponibili, tra cui la documentazione ufficiale di TypeScript, le comunità online e i progetti open-source. Abbracciate la potenza dei Tipi Mappati e dei Tipi Condizionali e sarete ben attrezzati per affrontare anche i problemi più impegnativi legati ai tipi.