Una guida completa alla parola chiave 'infer' di TypeScript, che spiega come usarla con i tipi condizionali per un'estrazione e manipolazione potenti dei tipi, inclusi casi d'uso avanzati.
Padroneggiare TypeScript Infer: Estrazione di Tipi Condizionali per la Manipolazione Avanzata dei Tipi
Il sistema di tipi di TypeScript è incredibilmente potente, consentendo agli sviluppatori di creare applicazioni robuste e manutenibili. Una delle caratteristiche chiave che abilita questa potenza è la parola chiave infer
usata in combinazione con i tipi condizionali. Questa combinazione fornisce un meccanismo per l'estrazione di tipi specifici da strutture di tipi complesse. Questo post del blog approfondisce la parola chiave infer
, spiegandone la funzionalità e mostrando casi d'uso avanzati. Esploreremo esempi pratici applicabili a diversi scenari di sviluppo software, dall'interazione con le API alla manipolazione di strutture di dati complesse.
Cosa sono i Tipi Condizionali?
Prima di immergerci in infer
, esaminiamo rapidamente i tipi condizionali. I tipi condizionali in TypeScript consentono di definire un tipo in base a una condizione, simile a un operatore ternario in JavaScript. La sintassi di base è:
T extends U ? X : Y
Questo si legge come: "Se il tipo T
è assegnabile al tipo U
, allora il tipo è X
; altrimenti, il tipo è Y
."
Esempio:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Introduzione alla Parola Chiave infer
La parola chiave infer
viene utilizzata all'interno della clausola extends
di un tipo condizionale per dichiarare una variabile di tipo che può essere dedotta dal tipo che viene controllato. In sostanza, consente di "catturare" una parte di un tipo per un uso successivo.
Sintassi di base:
type MyType<T> = T extends (infer U) ? U : never;
In questo esempio, se T
è assegnabile a un certo tipo, TypeScript tenterà di dedurre il tipo di U
. Se l'inferenza ha successo, il tipo sarà U
; altrimenti, sarà never
.
Esempi Semplici di infer
1. Inferire il Tipo di Ritorno di una Funzione
Un caso d'uso comune è l'inferenza del tipo di ritorno di una funzione:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number
function greet(name: string): string {
return `Hello, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
In questo esempio, ReturnType<T>
prende un tipo di funzione T
come input. Controlla se T
è assegnabile a una funzione che accetta qualsiasi argomento e restituisce un valore. In caso affermativo, deduce il tipo di ritorno come R
e lo restituisce. Altrimenti, restituisce any
.
2. Inferire il Tipo di Elemento di un Array
Un altro scenario utile è l'estrazione del tipo di elemento da un array:
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never
Qui, ArrayElementType<T>
controlla se T
è un tipo di array. In caso affermativo, deduce il tipo di elemento come U
e lo restituisce. In caso contrario, restituisce never
.
Casi d'Uso Avanzati di infer
1. Inferire i Parametri di un Costruttore
È possibile utilizzare infer
per estrarre i tipi di parametro di una funzione costruttore:
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Person {
constructor(public name: string, public age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]
class Point {
constructor(public x: number, public y: number) {}
}
type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]
In questo caso, ConstructorParameters<T>
prende un tipo di funzione costruttore T
. Deduce i tipi dei parametri del costruttore come P
e li restituisce come una tupla.
2. Estrazione di Proprietà dai Tipi Oggetto
infer
può anche essere utilizzato per estrarre proprietà specifiche dai tipi oggetto utilizzando tipi mappati e tipi condizionali:
type PickByType<T, K extends keyof T, U> = {
[P in K as T[P] extends U ? P : never]: T[P];
};
interface User {
id: number;
name: string;
age: number;
email: string;
isActive: boolean;
}
type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }
type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }
//An interface representing geographic coordinates.
interface GeoCoordinates {
latitude: number;
longitude: number;
altitude: number;
country: string;
city: string;
timezone: string;
}
type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }
Qui, PickByType<T, K, U>
crea un nuovo tipo che include solo le proprietà di T
(con chiavi in K
) i cui valori sono assegnabili al tipo U
. Il tipo mappato itera sulle chiavi di T
e il tipo condizionale filtra le chiavi che non corrispondono al tipo specificato.
3. Lavorare con le Promise
È possibile dedurre il tipo risolto di una Promise
:
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'Data from API';
}
type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string
async function fetchNumbers(): Promise<number[]> {
return [1, 2, 3];
}
type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]
Il tipo Awaited<T>
prende un tipo T
, che dovrebbe essere una Promise. Il tipo deduce quindi il tipo risolto U
della Promise e lo restituisce. Se T
non è una promise, restituisce T. Questo è un tipo di utilità integrato nelle versioni più recenti di TypeScript.
4. Estrazione del Tipo di un Array di Promise
La combinazione di Awaited
e l'inferenza del tipo di array consente di dedurre il tipo risolto da un array di Promise. Questo è particolarmente utile quando si ha a che fare con Promise.all
.
type PromiseArrayReturnType<T extends Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};
async function getUSDRate(): Promise<number> {
return 0.0069;
}
async function getEURRate(): Promise<number> {
return 0.0064;
}
const rates = [getUSDRate(), getEURRate()];
type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]
Questo esempio definisce prima due funzioni asincrone, getUSDRate
e getEURRate
, che simulano il recupero dei tassi di cambio. Il tipo di utilità PromiseArrayReturnType
estrae quindi il tipo risolto da ogni Promise
nell'array, risultando in un tipo di tupla in cui ogni elemento è il tipo atteso della Promise corrispondente.
Esempi Pratici in Diversi Domini
1. Applicazione di E-commerce
Considera un'applicazione di e-commerce in cui recuperi i dettagli del prodotto da un'API. Puoi usare infer
per estrarre il tipo dei dati del prodotto:
interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
category: string;
rating: number;
countryOfOrigin: string;
}
async function fetchProduct(productId: number): Promise<Product> {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Example Product',
price: 29.99,
description: 'A sample product',
imageUrl: 'https://example.com/image.jpg',
category: 'Electronics',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Product Name: ${product.name}`);
console.log(`Price: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
In questo esempio, definiamo un'interfaccia Product
e una funzione fetchProduct
che recupera i dettagli del prodotto da un'API. Usiamo Awaited
e ReturnType
per estrarre il tipo Product
dal tipo di ritorno della funzione fetchProduct
, consentendoci di controllare il tipo della funzione displayProductDetails
.
2. Internazionalizzazione (i18n)
Supponi di avere una funzione di traduzione che restituisce stringhe diverse in base alla lingua. È possibile utilizzare infer
per estrarre il tipo di ritorno di questa funzione per la sicurezza del tipo:
interface Translations {
greeting: string;
farewell: string;
welcomeMessage: (name: string) => string;
}
const enTranslations: Translations = {
greeting: 'Hello',
farewell: 'Goodbye',
welcomeMessage: (name: string) => `Welcome, ${name}!`;
};
const frTranslations: Translations = {
greeting: 'Bonjour',
farewell: 'Au revoir',
welcomeMessage: (name: string) => `Bienvenue, ${name}!`;
};
function getTranslation(locale: 'en' | 'fr'): Translations {
return locale === 'en' ? enTranslations : frTranslations;
}
type TranslationType = ReturnType<typeof getTranslation>;
function greetUser(locale: 'en' | 'fr', name: string) {
const translations = getTranslation(locale);
console.log(translations.welcomeMessage(name));
}
greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!
Qui, TranslationType
viene dedotto per essere l'interfaccia Translations
, garantendo che la funzione greetUser
abbia le informazioni sul tipo corrette per l'accesso alle stringhe tradotte.
3. Gestione delle Risposte API
Quando si lavora con le API, la struttura della risposta può essere complessa. infer
può aiutare a estrarre tipi di dati specifici dalle risposte API nidificate:
interface ApiResponse<T> {
status: number;
data: T;
message?: string;
}
interface UserData {
id: number;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
country: string;
language: string;
}
}
async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: 200,
data: {
id: userId,
username: 'johndoe',
email: 'john.doe@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
country: 'USA',
language: 'en'
}
}
});
}, 500);
});
}
type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;
type UserProfileType = UserApiResponse['data']['profile'];
function displayUserProfile(profile: UserProfileType) {
console.log(`Name: ${profile.firstName} ${profile.lastName}`);
console.log(`Country: ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
In questo esempio, definiamo un'interfaccia ApiResponse
e un'interfaccia UserData
. Usiamo infer
e l'indicizzazione dei tipi per estrarre UserProfileType
dalla risposta dell'API, garantendo che la funzione displayUserProfile
riceva il tipo corretto.
Best Practice per l'Uso di infer
- Mantieni Semplice: Usa
infer
solo quando necessario. Usarlo eccessivamente può rendere il tuo codice più difficile da leggere e comprendere. - Documenta i Tuoi Tipi: Aggiungi commenti per spiegare cosa stanno facendo i tuoi tipi condizionali e le istruzioni
infer
. - Testa i Tuoi Tipi: Usa il controllo dei tipi di TypeScript per assicurarti che i tuoi tipi si comportino come previsto.
- Considera le Prestazioni: I tipi condizionali complessi a volte possono influire sul tempo di compilazione. Sii consapevole della complessità dei tuoi tipi.
- Usa i Tipi di Utilità: TypeScript fornisce diversi tipi di utilità integrati (ad esempio,
ReturnType
,Awaited
) che possono semplificare il tuo codice e ridurre la necessità di istruzioniinfer
personalizzate.
Errori Comuni
- Inferenza Incorretta: A volte, TypeScript potrebbe dedurre un tipo che non è quello che ti aspetti. Ricontrolla le tue definizioni di tipo e le condizioni.
- Dipendenze Circolari: Fai attenzione quando definisci tipi ricorsivi usando
infer
, poiché possono portare a dipendenze circolari ed errori di compilazione. - Tipi Eccessivamente Complessi: Evita di creare tipi condizionali eccessivamente complessi che sono difficili da comprendere e mantenere. Suddividili in tipi più piccoli e gestibili.
Alternative a infer
Sebbene infer
sia uno strumento potente, ci sono situazioni in cui approcci alternativi potrebbero essere più appropriati:
- Asserzioni di Tipo: In alcuni casi, è possibile utilizzare le asserzioni di tipo per specificare esplicitamente il tipo di un valore anziché dedurlo. Tuttavia, fai attenzione con le asserzioni di tipo, poiché possono ignorare il controllo dei tipi.
- Guardie di Tipo: Le guardie di tipo possono essere utilizzate per restringere il tipo di un valore in base a controlli di runtime. Questo è utile quando è necessario gestire tipi diversi in base alle condizioni di runtime.
- Tipi di Utilità: TypeScript fornisce un ricco set di tipi di utilità che possono gestire molte attività comuni di manipolazione dei tipi senza la necessità di istruzioni
infer
personalizzate.
Conclusione
La parola chiave infer
in TypeScript, se combinata con i tipi condizionali, sblocca funzionalità avanzate di manipolazione dei tipi. Permette di estrarre tipi specifici da strutture di tipi complesse, consentendo di scrivere codice più robusto, manutenibile e a sicurezza di tipo. Dall'inferenza dei tipi di ritorno delle funzioni all'estrazione di proprietà dai tipi oggetto, le possibilità sono vaste. Comprendendo i principi e le best practice descritti in questa guida, è possibile sfruttare infer
al massimo delle sue potenzialità ed elevare le proprie competenze in TypeScript. Ricorda di documentare i tuoi tipi, testarli a fondo e considerare approcci alternativi quando appropriato. Padroneggiare infer
ti consente di scrivere codice TypeScript veramente espressivo e potente, portando in definitiva a un software migliore.