Explore recursos avançados do TypeScript, como tipos template literal e tipos condicionais, para escrever código mais expressivo e sustentável. Domine a manipulação de tipos para cenários complexos.
Tipos Avançados do TypeScript: Dominando Template Literal e Tipos Condicionais
A força do TypeScript reside em seu poderoso sistema de tipos. Embora os tipos básicos como string, number e boolean sejam suficientes para muitos cenários, recursos avançados como tipos template literal e tipos condicionais desbloqueiam um novo nível de expressividade e segurança de tipo. Este guia fornece uma visão geral abrangente desses tipos avançados, explorando suas capacidades e demonstrando aplicações práticas.
Entendendo Tipos Template Literal
Os tipos template literal se baseiam nos template literals do JavaScript, permitindo que você defina tipos com base na interpolação de strings. Isso permite a criação de tipos que representam padrões de string específicos, tornando seu código mais robusto e previsível.
Sintaxe Básica e Uso
Os tipos template literal usam crases (`) para envolver a definição do tipo, semelhante aos template literals do JavaScript. Dentro das crases, você pode interpolar outros tipos usando a sintaxe ${}. É aqui que a mágica acontece – você está essencialmente criando um tipo que é uma string, construída em tempo de compilação com base nos tipos dentro da interpolação.
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/${string}`;
// Exemplo de Uso
const getEndpoint: APIEndpoint = "/api/users"; // Válido
const postEndpoint: APIEndpoint = "/api/products/123"; // Válido
const invalidEndpoint: APIEndpoint = "/admin/settings"; // TypeScript não mostrará um erro aqui, pois `string` pode ser qualquer coisa
Neste exemplo, APIEndpoint é um tipo que representa qualquer string que comece com /api/. Embora este exemplo básico seja útil, o verdadeiro poder dos tipos template literal surge quando combinado com restrições de tipo mais específicas.
Combinando com Tipos Union
Os tipos template literal realmente brilham quando usados com tipos union. Isso permite que você crie tipos que representam um conjunto específico de combinações de string.
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIPath = "users" | "products" | "orders";
type APIEndpoint = `/${APIPath}/${HTTPMethod}`;
// Endpoints de API Válidos
const getUsers: APIEndpoint = "/users/GET";
const postProducts: APIEndpoint = "/products/POST";
// Endpoints de API Inválidos (resultarão em erros do TypeScript)
// const invalidEndpoint: APIEndpoint = "/users/PATCH"; // Error: "/users/PATCH" não é atribuível ao tipo "/users/GET" | "/users/POST" | "/users/PUT" | "/users/DELETE" | "/products/GET" | "/products/POST" | ... 3 more ... | "/orders/DELETE".
Agora, APIEndpoint é um tipo mais restritivo que permite apenas combinações específicas de caminhos de API e métodos HTTP. O TypeScript sinalizará qualquer tentativa de usar combinações inválidas, aumentando a segurança do tipo.
Manipulação de String com Tipos Template Literal
O TypeScript fornece tipos intrínsecos de manipulação de string que funcionam perfeitamente com tipos template literal. Esses tipos permitem que você transforme strings em tempo de compilação.
- Uppercase: Converte uma string para maiúsculas.
- Lowercase: Converte uma string para minúsculas.
- Capitalize: Coloca a primeira letra de uma string em maiúscula.
- Uncapitalize: Coloca a primeira letra de uma string em minúscula.
type Greeting = "hello world";
type UppercaseGreeting = Uppercase; // "HELLO WORLD"
type LowercaseGreeting = Lowercase; // "hello world"
type CapitalizedGreeting = Capitalize; // "Hello world"
type UncapitalizedGreeting = Uncapitalize; // "hello world"
Esses tipos de manipulação de string são particularmente úteis para gerar automaticamente tipos com base em convenções de nomenclatura. Por exemplo, você pode derivar tipos de ação de nomes de eventos ou vice-versa.
Aplicações Práticas de Tipos Template Literal
- Definição de Endpoint de API: Como demonstrado acima, definindo endpoints de API com restrições de tipo precisas.
- Manipulação de Eventos: Criando tipos para nomes de eventos com prefixos e sufixos específicos.
- Geração de Classe CSS: Gerando nomes de classe CSS com base em nomes de componentes e estados.
- Construção de Query de Banco de Dados: Garantindo a segurança do tipo ao construir consultas de banco de dados.
Exemplo Internacional: Formatação de Moeda
Imagine construir um aplicativo financeiro que suporte várias moedas. Você pode usar tipos template literal para impor a formatação correta da moeda.
type CurrencyCode = "USD" | "EUR" | "GBP" | "JPY";
type CurrencyFormat = `${number} ${T}`;
const priceUSD: CurrencyFormat<"USD"> = "100 USD"; // Válido
const priceEUR: CurrencyFormat<"EUR"> = "50 EUR"; // Válido
// const priceInvalid: CurrencyFormat<"USD"> = "100 EUR"; // Error: Type 'string' is not assignable to type '`${number} USD`'.
function formatCurrency(amount: number, currency: T): CurrencyFormat {
return `${amount} ${currency}`;
}
const formattedUSD = formatCurrency(250, "USD"); // Type: "250 USD"
const formattedEUR = formatCurrency(100, "EUR"); // Type: "100 EUR"
Este exemplo garante que os valores de moeda sejam sempre formatados com o código de moeda correto, evitando possíveis erros.
Aprofundando-se em Tipos Condicionais
Os tipos condicionais introduzem lógica de ramificação no sistema de tipos do TypeScript, permitindo que você defina tipos que dependem de outros tipos. Este recurso é incrivelmente poderoso para criar definições de tipo altamente flexíveis e reutilizáveis.
Sintaxe Básica e Uso
Os tipos condicionais usam a palavra-chave infer e o operador ternário (condition ? trueType : falseType) para definir condições de tipo.
type IsString = T extends string ? true : false;
type StringCheck = IsString; // type StringCheck = true
type NumberCheck = IsString; // type NumberCheck = false
Neste exemplo, IsString é um tipo condicional que verifica se T é atribuível a string. Se for, o tipo é resolvido para true; caso contrário, é resolvido para false.
A Palavra-Chave infer
A palavra-chave infer permite que você extraia um tipo de um tipo. Isso é particularmente útil ao trabalhar com tipos complexos, como tipos de função ou tipos de array.
type ReturnType any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType; // type AddReturnType = number
Neste exemplo, ReturnType extrai o tipo de retorno de um tipo de função T. A parte infer R do tipo condicional infere o tipo de retorno e o atribui à variável de tipo R. Se T não for um tipo de função, o tipo é resolvido para any.
Tipos Condicionais Distributivos
Os tipos condicionais tornam-se distributivos quando o tipo verificado é um parâmetro de tipo naked. Isso significa que o tipo condicional é aplicado a cada membro do tipo union separadamente.
type ToArray = T extends any ? T[] : never;
type NumberOrStringArray = ToArray; // type NumberOrStringArray = string[] | number[]
Neste exemplo, ToArray converte um tipo T em um tipo de array. Como T é um parâmetro de tipo naked (não envolvido em outro tipo), o tipo condicional é aplicado a number e string separadamente, resultando em uma união de number[] e string[].
Aplicações Práticas de Tipos Condicionais
- Extraindo Tipos de Retorno: Como demonstrado acima, extraindo o tipo de retorno de uma função.
- Filtrando Tipos de uma Union: Criando um tipo que contenha apenas tipos específicos de uma union.
- Definindo Tipos de Função Sobrecarregados: Criando diferentes tipos de função com base nos tipos de entrada.
- Criando Type Guards: Definindo funções que restringem o tipo de uma variável.
Exemplo Internacional: Lidando com Diferentes Formatos de Data
Diferentes regiões do mundo usam diferentes formatos de data. Você pode usar tipos condicionais para lidar com essas variações.
type DateFormat = "YYYY-MM-DD" | "MM/DD/YYYY" | "DD.MM.YYYY";
type ParseDate = T extends "YYYY-MM-DD"
? { year: number; month: number; day: number; format: "YYYY-MM-DD" }
: T extends "MM/DD/YYYY"
? { month: number; day: number; year: number; format: "MM/DD/YYYY" }
: T extends "DD.MM.YYYY"
? { day: number; month: number; year: number; format: "DD.MM.YYYY" }
: never;
function parseDate(dateString: string, format: T): ParseDate {
// (Implementação lidaria com diferentes formatos de data)
if (format === "YYYY-MM-DD") {
const [year, month, day] = dateString.split("-").map(Number);
return { year, month, day, format } as ParseDate;
} else if (format === "MM/DD/YYYY") {
const [month, day, year] = dateString.split("/").map(Number);
return { month, day, year, format } as ParseDate;
} else if (format === "DD.MM.YYYY") {
const [day, month, year] = dateString.split(".").map(Number);
return { day, month, year, format } as ParseDate;
} else {
throw new Error("Invalid date format");
}
}
const parsedDateISO = parseDate("2023-10-27", "YYYY-MM-DD"); // Type: { year: number; month: number; day: number; format: "YYYY-MM-DD"; }
const parsedDateUS = parseDate("10/27/2023", "MM/DD/YYYY"); // Type: { month: number; day: number; year: number; format: "MM/DD/YYYY"; }
const parsedDateEU = parseDate("27.10.2023", "DD.MM.YYYY"); // Type: { day: number; month: number; year: number; format: "DD.MM.YYYY"; }
console.log(parsedDateISO.year); // Acessa o ano sabendo que estará lá
Este exemplo usa tipos condicionais para definir diferentes funções de análise de data com base no formato de data especificado. O tipo ParseDate garante que o objeto retornado tenha as propriedades corretas com base no formato.
Combinando Tipos Template Literal e Condicionais
O verdadeiro poder surge quando você combina tipos template literal e tipos condicionais. Isso permite manipulações de tipo incrivelmente poderosas.
type EventName = `on${Capitalize}`;
type ExtractEventPayload = T extends EventName
? { type: T; payload: any } // Simplificado para demonstração
: never;
type ClickEvent = EventName<"click">; // "onClick"
type MouseOverEvent = EventName<"mouseOver">; // "onMouseOver"
//Exemplo de função que recebe um tipo
function processEvent(event: T): ExtractEventPayload {
//Em uma implementação real, realmente despacharíamos o evento.
console.log(`Processing event ${event}`);
//Em uma implementação real, a carga útil seria baseada no tipo de evento.
return { type: event, payload: {} } as ExtractEventPayload;
}
//Observe que os tipos de retorno são muito específicos:
const clickEvent = processEvent("onClick"); // { type: "onClick"; payload: any; }
const mouseOverEvent = processEvent("onMouseOver"); // { type: "onMouseOver"; payload: any; }
//Se você usar outras strings, você recebe never:
// const someOtherEvent = processEvent("someOtherEvent"); // Type is `never`
Melhores Práticas e Considerações
- Mantenha Simples: Embora poderosos, esses tipos avançados podem se tornar complexos rapidamente. Esforce-se para clareza e sustentabilidade.
- Teste Exaustivamente: Garanta que suas definições de tipo se comportem conforme o esperado, escrevendo testes de unidade abrangentes.
- Documente Seu Código: Documente claramente o propósito e o comportamento de seus tipos avançados para melhorar a legibilidade do código.
- Considere o Desempenho: O uso excessivo de tipos avançados pode afetar o tempo de compilação. Analise seu código e otimize quando necessário.
Conclusão
Tipos template literal e tipos condicionais são ferramentas poderosas no arsenal do TypeScript. Ao dominar esses tipos avançados, você pode escrever código mais expressivo, sustentável e com segurança de tipo. Esses recursos permitem que você capture relacionamentos complexos entre tipos, imponha restrições mais rígidas e crie definições de tipo altamente reutilizáveis. Abrace essas técnicas para elevar suas habilidades de TypeScript e construir aplicações robustas e escaláveis para um público global.