Explore tipos de marcação em TypeScript, uma técnica poderosa para alcançar digitação nominal em um sistema de tipos estrutural. Melhore a segurança de tipos e a clareza do código.
Tipos de Marcação em TypeScript: Digitação Nominal em um Sistema Estrutural
O sistema de tipos estruturais do TypeScript oferece flexibilidade, mas às vezes pode levar a comportamentos inesperados. Tipos de marcação fornecem uma maneira de impor a digitação nominal, aprimorando a segurança de tipos e a clareza do código. Este artigo explora tipos de marcação em detalhes, fornecendo exemplos práticos e melhores práticas para sua implementação.
Compreendendo a Digitação Estrutural vs. Nominal
Antes de mergulharmos nos tipos de marcação, vamos esclarecer a diferença entre digitação estrutural e nominal.
Digitação Estrutural (Duck Typing)
Em um sistema de tipos estruturais, dois tipos são considerados compatíveis se tiverem a mesma estrutura (ou seja, as mesmas propriedades com os mesmos tipos). O TypeScript usa digitação estrutural. Considere este exemplo:
interface Point {
x: number;
y: number;
}
interface Vector {
x: number;
y: number;
}
const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Válido em TypeScript
console.log(vector.x); // Saída: 10
Mesmo que Point
e Vector
sejam declarados como tipos distintos, o TypeScript permite atribuir um objeto Point
a uma variável Vector
porque eles compartilham a mesma estrutura. Isso pode ser conveniente, mas também pode levar a erros se você precisar distinguir entre tipos logicamente diferentes que, por acaso, têm a mesma forma. Por exemplo, pensar em coordenadas de latitude/longitude que podem coincidir incidentalmente com coordenadas de pixels da tela.
Digitação Nominal
Em um sistema de tipos nominal, os tipos são considerados compatíveis apenas se tiverem o mesmo nome. Mesmo que dois tipos tenham a mesma estrutura, eles são tratados como distintos se tiverem nomes diferentes. Linguagens como Java e C# usam digitação nominal.
A Necessidade de Tipos de Marcação
A digitação estrutural do TypeScript pode ser problemática quando você precisa garantir que um valor pertença a um tipo específico, independentemente de sua estrutura. Por exemplo, considere a representação de moedas. Você pode ter tipos diferentes para USD e EUR, mas ambos podem ser representados como números. Sem um mecanismo para distingui-los, você pode acidentalmente realizar operações na moeda errada.
Tipos de marcação resolvem esse problema permitindo que você crie tipos distintos que são estruturalmente semelhantes, mas tratados como diferentes pelo sistema de tipos. Isso aprimora a segurança de tipos e previne erros que, de outra forma, poderiam passar despercebidos.
Implementando Tipos de Marcação em TypeScript
Tipos de marcação são implementados usando tipos de interseção e um símbolo ou literal de string único. A ideia é adicionar uma "marca" a um tipo que o distingue de outros tipos com a mesma estrutura.
Usando Símbolos (Recomendado)
O uso de símbolos para marcação é geralmente preferido porque os símbolos são garantidos como únicos.
const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };
const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);
// Descomentar a linha abaixo causará um erro de tipo
// const invalidOperation = addUSD(usd1, eur1);
Neste exemplo, USD
e EUR
são tipos de marcação baseados no tipo number
. O unique symbol
garante que esses tipos sejam distintos. As funções createUSD
e createEUR
são usadas para criar valores desses tipos, e a função addUSD
aceita apenas valores USD
. Tentar somar um valor EUR
a um valor USD
resultará em um erro de tipo.
Usando Literais de String
Você também pode usar literais de string para marcação, embora essa abordagem seja menos robusta do que usar símbolos, pois os literais de string não são garantidos como únicos.
type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);
// Descomentar a linha abaixo causará um erro de tipo
// const invalidOperation = addUSD(usd1, eur1);
Este exemplo atinge o mesmo resultado do anterior, mas usando literais de string em vez de símbolos. Embora mais simples, é importante garantir que os literais de string usados para marcação sejam únicos em seu codebase.
Exemplos Práticos e Casos de Uso
Tipos de marcação podem ser aplicados a vários cenários onde você precisa impor a segurança de tipos além da compatibilidade estrutural.
IDs
Considere um sistema com diferentes tipos de IDs, como UserID
, ProductID
e OrderID
. Todos esses IDs podem ser representados como números ou strings, mas você deseja evitar a mistura acidental de diferentes tipos de ID.
const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };
const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };
function getUser(id: UserID): { name: string } {
// ... buscar dados do usuário
return { name: "Alice" };
}
function getProduct(id: ProductID): { name: string, price: number } {
// ... buscar dados do produto
return { name: "Exemplo de Produto", price: 25 };
}
function createUserID(id: string): UserID {
return id as UserID;
}
function createProductID(id: string): ProductID {
return id as ProductID;
}
const userID = createUserID('user123');
const productID = createProductID('product456');
const user = getUser(userID);
const product = getProduct(productID);
console.log("Usuário:", user);
console.log("Produto:", product);
// Descomentar a linha abaixo causará um erro de tipo
// const invalidCall = getUser(productID);
Este exemplo demonstra como os tipos de marcação podem impedir a passagem de um ProductID
para uma função que espera um UserID
, aprimorando a segurança de tipos.
Valores Específicos do Domínio
Tipos de marcação também podem ser úteis para representar valores específicos do domínio com restrições. Por exemplo, você pode ter um tipo para porcentagens que deve sempre estar entre 0 e 100.
const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };
function createPercentage(value: number): Percentage {
if (value < 0 || value > 100) {
throw new Error('Porcentagem deve estar entre 0 e 100');
}
return value as Percentage;
}
function applyDiscount(price: number, discount: Percentage): number {
return price * (1 - discount / 100);
}
try {
const discount = createPercentage(20);
const discountedPrice = applyDiscount(100, discount);
console.log("Preço com Desconto:", discountedPrice);
// Descomentar a linha abaixo causará um erro em tempo de execução
// const invalidPercentage = createPercentage(120);
} catch (error) {
console.error(error);
}
Este exemplo mostra como impor uma restrição ao valor de um tipo de marcação em tempo de execução. Embora o sistema de tipos não possa garantir que um valor de Percentage
esteja sempre entre 0 e 100, a função createPercentage
pode impor essa restrição em tempo de execução. Você também pode usar bibliotecas como io-ts para impor validação em tempo de execução de tipos de marcação.
Representações de Data e Hora
Trabalhar com datas e horas pode ser complicado devido a vários formatos e fusos horários. Tipos de marcação podem ajudar a diferenciar entre diferentes representações de data e hora.
const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };
const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };
function createUTCDate(dateString: string): UTCDate {
// Validar que a string de data está no formato UTC (por exemplo, ISO 8601 com Z)
if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
throw new Error('Formato de data UTC inválido');
}
return dateString as UTCDate;
}
function createLocalDate(dateString: string): LocalDate {
// Validar que a string de data está no formato de data local (por exemplo, YYYY-MM-DD)
if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
throw new Error('Formato de data local inválido');
}
return dateString as LocalDate;
}
function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
// Realizar conversão de fuso horário
const date = new Date(utcDate);
const localDateString = date.toLocaleDateString();
return createLocalDate(localDateString);
}
try {
const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
const localDate = convertUTCDateToLocalDate(utcDate);
console.log("Data UTC:", utcDate);
console.log("Data Local:", localDate);
} catch (error) {
console.error(error);
}
Este exemplo diferencia datas UTC e locais, garantindo que você esteja trabalhando com a representação correta de data e hora em diferentes partes de sua aplicação. A validação em tempo de execução garante que apenas strings de data formatadas corretamente possam ser atribuídas a esses tipos.
Melhores Práticas para Usar Tipos de Marcação
Para usar efetivamente tipos de marcação em TypeScript, considere as seguintes melhores práticas:
- Use Símbolos para Marcação: Símbolos fornecem a garantia mais forte de exclusividade, reduzindo o risco de erros de tipo.
- Crie Funções Auxiliares: Use funções auxiliares para criar valores de tipos de marcação. Isso fornece um ponto central para validação e garante consistência.
- Aplique Validação em Tempo de Execução: Embora tipos de marcação aprimorem a segurança de tipos, eles não impedem que valores incorretos sejam atribuídos em tempo de execução. Use validação em tempo de execução para impor restrições.
- Documente Tipos de Marcação: Documente claramente o propósito e as restrições de cada tipo de marcação para melhorar a manutenibilidade do código.
- Considere as Implicações de Desempenho: Tipos de marcação introduzem uma pequena sobrecarga devido ao tipo de interseção e à necessidade de funções auxiliares. Considere o impacto no desempenho em seções críticas de desempenho do seu código.
Vantagens dos Tipos de Marcação
- Segurança de Tipos Aprimorada: Impede a mistura acidental de tipos estruturalmente semelhantes, mas logicamente diferentes.
- Clareza de Código Melhorada: Torna o código mais legível e fácil de entender, diferenciando explicitamente entre tipos.
- Redução de Erros: Detecta erros potenciais em tempo de compilação, reduzindo o risco de bugs em tempo de execução.
- Manutenibilidade Aumentada: Torna o código mais fácil de manter e refatorar, fornecendo uma separação clara de responsabilidades.
Desvantagens dos Tipos de Marcação
- Complexidade Aumentada: Adiciona complexidade ao codebase, especialmente ao lidar com muitos tipos de marcação.
- Sobrecarga em Tempo de Execução: Introduz uma pequena sobrecarga em tempo de execução devido à necessidade de funções auxiliares e validação em tempo de execução.
- Potencial para Código Repetitivo: Pode levar a código repetitivo, especialmente ao criar e validar tipos de marcação.
Alternativas aos Tipos de Marcação
Embora tipos de marcação sejam uma técnica poderosa para alcançar digitação nominal em TypeScript, existem abordagens alternativas que você pode considerar.
Tipos Opacos
Tipos opacos são semelhantes a tipos de marcação, mas fornecem uma maneira mais explícita de ocultar o tipo subjacente. O TypeScript não tem suporte integrado para tipos opacos, mas você pode simulá-los usando módulos e símbolos privados.
Classes
O uso de classes pode fornecer uma abordagem mais orientada a objetos para definir tipos distintos. Embora as classes sejam tipadas estruturalmente em TypeScript, elas oferecem uma separação de responsabilidades mais clara e podem ser usadas para impor restrições por meio de métodos.
Bibliotecas como `io-ts` ou `zod`
Essas bibliotecas fornecem validação sofisticada de tipos em tempo de execução e podem ser combinadas com tipos de marcação para garantir segurança em tempo de compilação e execução.
Conclusão
Tipos de marcação em TypeScript são uma ferramenta valiosa para aprimorar a segurança de tipos e a clareza do código em um sistema de tipos estrutural. Ao adicionar uma "marca" a um tipo, você pode impor a digitação nominal e evitar a mistura acidental de tipos estruturalmente semelhantes, mas logicamente diferentes. Embora os tipos de marcação introduzam alguma complexidade e sobrecarga, os benefícios de segurança de tipos e manutenibilidade do código aprimorados geralmente superam as desvantagens. Considere usar tipos de marcação em cenários onde você precise garantir que um valor pertença a um tipo específico, independentemente de sua estrutura.
Ao entender os princípios por trás da digitação estrutural e nominal, e aplicando as melhores práticas descritas neste artigo, você pode alavancar efetivamente os tipos de marcação para escrever código TypeScript mais robusto e de fácil manutenção. Desde a representação de moedas e IDs até a imposição de restrições específicas do domínio, os tipos de marcação fornecem um mecanismo flexível e poderoso para aprimorar a segurança de tipos em seus projetos.
Ao trabalhar com TypeScript, explore as várias técnicas e bibliotecas disponíveis para validação e imposição de tipos. Considere usar tipos de marcação em conjunto com bibliotecas de validação em tempo de execução como io-ts
ou zod
para alcançar uma abordagem abrangente à segurança de tipos.