Italiano

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

Errori Comuni

Alternative a infer

Sebbene infer sia uno strumento potente, ci sono situazioni in cui approcci alternativi potrebbero essere più appropriati:

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.