Italiano

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;
};

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

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.

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

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.