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
- Mantenha Simples: Use
infer
apenas quando necessário. O uso excessivo pode tornar seu código mais difícil de ler e entender. - Documente Seus Tipos: Adicione comentários para explicar o que seus tipos condicionais e declarações
infer
estão fazendo. - Teste Seus Tipos: Use a verificação de tipo do TypeScript para garantir que seus tipos estejam se comportando como esperado.
- Considere o Desempenho: Tipos condicionais complexos às vezes podem impactar o tempo de compilação. Esteja atento à complexidade de seus tipos.
- Use Tipos de Utilitários: O TypeScript fornece vários tipos de utilitários integrados (por exemplo,
ReturnType
,Awaited
) que podem simplificar seu código e reduzir a necessidade de declaraçõesinfer
personalizadas.
Armadilhas Comuns
- Inferência Incorreta: Às vezes, o TypeScript pode inferir um tipo que não é o que você espera. Verifique novamente suas definições e condições de tipo.
- Dependências Circulares: Tenha cuidado ao definir tipos recursivos usando
infer
, pois eles podem levar a dependências circulares e erros de compilação. - Tipos Excessivamente Complexos: Evite criar tipos condicionais excessivamente complexos que sejam difíceis de entender e manter. Divida-os em tipos menores e mais gerenciáveis.
Alternativas para infer
Embora infer
seja uma ferramenta poderosa, existem situações em que abordagens alternativas podem ser mais apropriadas:
- Afirmações de Tipo: Em alguns casos, você pode usar afirmações de tipo para especificar explicitamente o tipo de um valor em vez de inferi-lo. No entanto, tenha cuidado com as afirmações de tipo, pois elas podem ignorar a verificação de tipo.
- Guardas de Tipo: Guardas de tipo podem ser usados para restringir o tipo de um valor com base em verificações de tempo de execução. Isso é útil quando você precisa lidar com diferentes tipos com base em condições de tempo de execução.
- Tipos de Utilitários: O TypeScript fornece um conjunto rico de tipos de utilitários que podem lidar com muitas tarefas comuns de manipulação de tipo sem a necessidade de declarações
infer
personalizadas.
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.