Português

Um guia completo para a palavra-chave 'infer' do TypeScript, explicando como usá-la com tipos condicionais para extração e manipulação poderosa de tipos, incluindo casos de uso avançados.

Dominando o Infer do TypeScript: Extração Condicional de Tipos para Manipulação Avançada de Tipos

O sistema de tipos do TypeScript é incrivelmente poderoso, permitindo que os desenvolvedores criem aplicações robustas e de fácil manutenção. Uma das principais características que permitem esse poder é a palavra-chave infer usada em conjunto com tipos condicionais. Essa combinação fornece um mecanismo para extrair tipos específicos de estruturas de tipos complexas. Este post do blog se aprofunda na palavra-chave infer, explicando sua funcionalidade e mostrando casos de uso avançados. Exploraremos exemplos práticos aplicáveis a diversos cenários de desenvolvimento de software, desde a interação com APIs até a manipulação de estruturas de dados complexas.

O que são Tipos Condicionais?

Antes de mergulharmos no infer, vamos revisar rapidamente os tipos condicionais. Os tipos condicionais no TypeScript permitem que você defina um tipo com base em uma condição, semelhante a um operador ternário no JavaScript. A sintaxe básica é:

T extends U ? X : Y

Isso é lido como: "Se o tipo T for atribuível ao tipo U, então o tipo é X; caso contrário, o tipo é Y."

Exemplo:

type IsString<T> = T extends string ? true : false;

type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false

Apresentando a palavra-chave infer

A palavra-chave infer é usada dentro da cláusula extends de um tipo condicional para declarar uma variável de tipo que pode ser inferida do tipo que está sendo verificado. Em essência, ele permite que você "capture" uma parte de um tipo para uso posterior.

Sintaxe Básica:

type MyType<T> = T extends (infer U) ? U : never;

Neste exemplo, se T for atribuível a algum tipo, o TypeScript tentará inferir o tipo de U. Se a inferência for bem-sucedida, o tipo será U; caso contrário, será never.

Exemplos Simples de infer

1. Inferindo o Tipo de Retorno de uma Função

Um caso de uso comum é inferir o tipo de retorno de uma função:

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

Neste exemplo, ReturnType<T> recebe um tipo de função T como entrada. Ele verifica se T é atribuível a uma função que aceita quaisquer argumentos e retorna um valor. Se for, ele infere o tipo de retorno como R e o retorna. Caso contrário, retorna any.

2. Inferindo o Tipo de Elemento de Array

Outro cenário útil é extrair o tipo de elemento de um 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

Aqui, ArrayElementType<T> verifica se T é um tipo de array. Se for, ele infere o tipo de elemento como U e o retorna. Se não, retorna never.

Casos de Uso Avançados de infer

1. Inferindo Parâmetros de um Construtor

Você pode usar infer para extrair os tipos de parâmetro de uma função construtora:

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]

Neste caso, ConstructorParameters<T> recebe um tipo de função construtora T. Ele infere os tipos dos parâmetros do construtor como P e os retorna como uma tupla.

2. Extraindo Propriedades de Tipos de Objeto

infer também pode ser usado para extrair propriedades específicas de tipos de objeto usando tipos mapeados e tipos condicionais:

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

Aqui, PickByType<T, K, U> cria um novo tipo que inclui apenas as propriedades de T (com chaves em K) cujos valores são atribuíveis ao tipo U. O tipo mapeado itera sobre as chaves de T, e o tipo condicional filtra as chaves que não correspondem ao tipo especificado.

3. Trabalhando com Promises

Você pode inferir o tipo resolvido de uma 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[]

O tipo Awaited<T> recebe um tipo T, que deve ser uma Promise. O tipo então infere o tipo resolvido U da Promise e o retorna. Se T não for uma promise, ele retorna T. Este é um tipo de utilitário integrado em versões mais recentes do TypeScript.

4. Extraindo o Tipo de um Array de Promises

A combinação de Awaited e a inferência de tipo de array permite inferir o tipo resolvido por um array de Promises. Isso é particularmente útil ao lidar com 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]

Este exemplo primeiro define duas funções assíncronas, getUSDRate e getEURRate, que simulam a busca de taxas de câmbio. O tipo de utilitário PromiseArrayReturnType então extrai o tipo resolvido de cada Promise no array, resultando em um tipo de tupla onde cada elemento é o tipo aguardado da Promise correspondente.

Exemplos Práticos em Diferentes Domínios

1. Aplicação de E-commerce

Considere uma aplicação de e-commerce onde você busca detalhes do produto de uma API. Você pode usar infer para extrair o tipo dos dados do produto:

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

Neste exemplo, definimos uma interface Product e uma função fetchProduct que busca detalhes do produto de uma API. Usamos Awaited e ReturnType para extrair o tipo Product do tipo de retorno da função fetchProduct, permitindo-nos verificar o tipo da função displayProductDetails.

2. Internacionalização (i18n)

Suponha que você tenha uma função de tradução que retorna diferentes strings com base na localidade. Você pode usar infer para extrair o tipo de retorno desta função para segurança de 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!

Aqui, o TranslationType é inferido como a interface Translations, garantindo que a função greetUser tenha as informações de tipo corretas para acessar strings traduzidas.

3. Tratamento de Resposta da API

Ao trabalhar com APIs, a estrutura da resposta pode ser complexa. infer pode ajudar a extrair tipos de dados específicos de respostas de API aninhadas:

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

Neste exemplo, definimos uma interface ApiResponse e uma interface UserData. Usamos infer e indexação de tipo para extrair o UserProfileType da resposta da API, garantindo que a função displayUserProfile receba o tipo correto.

Melhores Práticas para Usar infer

Armadilhas Comuns

Alternativas para infer

Embora infer seja uma ferramenta poderosa, existem situações em que abordagens alternativas podem ser mais apropriadas:

Conclusão

A palavra-chave infer no TypeScript, quando combinada com tipos condicionais, desbloqueia capacidades avançadas de manipulação de tipo. Ele permite que você extraia tipos específicos de estruturas de tipo complexas, permitindo que você escreva código mais robusto, de fácil manutenção e com tipo seguro. Desde inferir tipos de retorno de função até extrair propriedades de tipos de objeto, as possibilidades são vastas. Ao entender os princípios e as melhores práticas descritas neste guia, você pode aproveitar infer em todo o seu potencial e elevar suas habilidades em TypeScript. Lembre-se de documentar seus tipos, testá-los completamente e considerar abordagens alternativas quando apropriado. Dominar infer capacita você a escrever código TypeScript verdadeiramente expressivo e poderoso, levando, em última análise, a um software melhor.