Domine os tipos condicionais e mapeados do TypeScript para criar aplicações flexíveis e seguras. Saiba como escrever código dinâmico.
Padrões Avançados de TypeScript: Maestria em Tipos Condicionais e Mapeados
O poder do TypeScript reside na sua capacidade de fornecer tipagem forte, permitindo que você capture erros precocemente e escreva código mais fácil de manter. Enquanto tipos básicos como string
, number
e boolean
são fundamentais, os recursos avançados do TypeScript, como tipos condicionais e mapeados, desbloqueiam uma nova dimensão de flexibilidade e segurança de tipos. Este guia abrangente irá aprofundar esses conceitos poderosos, equipando você com o conhecimento para criar aplicações TypeScript verdadeiramente dinâmicas e adaptáveis.
O que são Tipos Condicionais?
Tipos condicionais permitem que você defina tipos que dependem de uma condição, semelhante a um operador ternário em JavaScript (condição ? valorVerdadeiro : valorFalso
). Eles permitem que você expresse relações de tipo complexas com base se um tipo satisfaz uma restrição específica.
Sintaxe
A sintaxe básica para um tipo condicional é:
T estende U ? X : Y
T
: O tipo que está sendo verificado.U
: O tipo a ser verificado.estende
: A palavra-chave que indica uma relação de subtipo.X
: O tipo a ser usado seT
for atribuível aU
.Y
: O tipo a ser usado seT
não for atribuível aU
.
Em essência, se T estende U
for avaliado como verdadeiro, o tipo se resolve para X
; caso contrário, ele se resolve para Y
.
Exemplos Práticos
1. Determinando o Tipo de um Parâmetro de Função
Vamos supor que você queira criar um tipo que determine se um parâmetro de função é uma string ou um número:
type TipoParametro<T> = T estende string ? string : number;
function processaValor(valor: TipoParametro<string | number>): void {
if (typeof valor === "string") {
console.log("Valor é uma string:", valor);
} else {
console.log("Valor é um número:", valor);
}
}
processaValor("olá"); // Saída: Valor é uma string: olá
processaValor(123); // Saída: Valor é um número: 123
Neste exemplo, TipoParametro<T>
é um tipo condicional. Se T
for uma string, o tipo se resolve para string
; caso contrário, ele se resolve para number
. A função processaValor
aceita uma string ou um número com base neste tipo condicional.
2. Extraindo o Tipo de Retorno com Base no Tipo de Entrada
Imagine um cenário em que você tem uma função que retorna tipos diferentes com base na entrada. Tipos condicionais podem ajudá-lo a definir o tipo de retorno correto:
interface ProcessadorString {
process(input: string): number;
}
interface ProcessadorNumero {
process(input: number): string;
}
type Processador<T> = T estende string ? ProcessadorString : ProcessadorNumero;
function criaProcessador<T estende string | number>(input: T): Processador<T> {
if (typeof input === "string") {
return { process: (input: string) => input.length } as Processador<T>;
} else {
return { process: (input: number) => input.toString() } as Processador<T>;
}
}
const processadorString = criaProcessador("exemplo");
const processadorNumero = criaProcessador(42);
console.log(processadorString.process("exemplo")); // Saída: 7
console.log(processadorNumero.process(42)); // Saída: "42"
Aqui, o tipo Processador<T>
seleciona condicionalmente ProcessadorString
ou ProcessadorNumero
com base no tipo da entrada. Isso garante que a função criaProcessador
retorne o tipo correto de objeto processador.
3. Uniões Discriminadas
Tipos condicionais são extremamente poderosos ao trabalhar com uniões discriminadas. Uma união discriminada é um tipo de união onde cada membro tem uma propriedade comum e de tipo singleton (o discriminante). Isso permite que você restrinja o tipo com base no valor dessa propriedade.
interface Quadrado {
kind: "quadrado";
size: number;
}
interface Circulo {
kind: "circulo";
radius: number;
}
type Forma = Quadrado | Circulo;
type Area<T estende Forma> = T estende { kind: "quadrado" } ? number : string;
function calculaArea(forma: Forma): Area<typeof forma> {
if (forma.kind === "quadrado") {
return forma.size * forma.size;
} else {
return Math.PI * forma.radius * forma.radius;
}
}
const meuQuadrado: Quadrado = { kind: "quadrado", size: 5 };
const meuCirculo: Circulo = { kind: "circulo", radius: 3 };
console.log(calculaArea(meuQuadrado)); // Saída: 25
console.log(calculaArea(meuCirculo)); // Saída: 28.274333882308138
Neste exemplo, o tipo Forma
é uma união discriminada. O tipo Area<T>
usa um tipo condicional para determinar se a forma é um quadrado ou um círculo, retornando um number
para quadrados e uma string
para círculos (embora em um cenário do mundo real, você provavelmente queira tipos de retorno consistentes, isso demonstra o princípio).
Principais Conclusões sobre Tipos Condicionais
- Permitem definir tipos com base em condições.
- Melhoram a segurança de tipos expressando relações de tipo complexas.
- São úteis para trabalhar com parâmetros de função, tipos de retorno e uniões discriminadas.
O que são Tipos Mapeados?
Tipos mapeados fornecem uma maneira de transformar tipos existentes mapeando suas propriedades. Eles permitem que você crie novos tipos com base nas propriedades de outro tipo, aplicando modificações como tornar propriedades opcionais, somente leitura ou alterar seus tipos.
Sintaxe
A sintaxe geral para um tipo mapeado é:
type NovoTipo<T> = {
[K in chaveDeT]: TipoModificado;
};
T
: O tipo de entrada.chaveDeT
: Um operador de tipo que retorna uma união de todas as chaves de propriedade emT
.K em chaveDeT
: Itera sobre cada chave emchaveDeT
, atribuindo cada chave à variável de tipoK
.TipoModificado
: O tipo para o qual cada propriedade será mapeada. Isso pode incluir tipos condicionais ou outras transformações de tipo.
Exemplos Práticos
1. Tornando Propriedades Opcionais
Você pode usar um tipo mapeado para tornar todas as propriedades de um tipo existente opcionais:
interface Usuario {
id: number;
name: string;
email: string;
}
type UsuarioParcial = {
[K em chaveDeUsuario]?: Usuario[K];
};
const usuarioParcial: UsuarioParcial = {
name: "John Doe",
}; // Válido, pois 'id' e 'email' são opcionais
Aqui, UsuarioParcial
é um tipo mapeado que itera sobre as chaves da interface Usuario
. Para cada chave K
, ele torna a propriedade opcional adicionando o modificador ?
. O Usuario[K]
recupera o tipo da propriedade K
da interface Usuario
.
2. Tornando Propriedades Somente Leitura
Da mesma forma, você pode tornar todas as propriedades de um tipo existente somente leitura:
interface Produto {
id: number;
name: string;
price: number;
}
type ProdutoSomenteLeitura = {
readonly [K em chaveDeProduto]: Produto[K];
};
const produtoSomenteLeitura: ProdutoSomenteLeitura = {
id: 123,
name: "Produto de Exemplo",
price: 25.00,
};
// produtoSomenteLeitura.price = 30.00; // Erro: Não é possível atribuir a 'price' porque é uma propriedade somente leitura.
Neste caso, ProdutoSomenteLeitura
é um tipo mapeado que adiciona o modificador readonly
a cada propriedade da interface Produto
.
3. Transformando Tipos de Propriedade
Tipos mapeados também podem ser usados para transformar os tipos de propriedades. Por exemplo, você pode criar um tipo que converte todas as propriedades de string em números:
interface Configuracao {
apiUrl: string;
timeout: string;
maxRetries: number;
}
type ConfiguracaoNumerica = {
[K em chaveDeConfiguracao]: Configuracao[K] estende string ? number : Configuracao[K];
};
const configuracaoNumerica: ConfiguracaoNumerica = {
apiUrl: 123, // Deve ser um número devido ao mapeamento
timeout: 456, // Deve ser um número devido ao mapeamento
maxRetries: 3,
};
Este exemplo demonstra o uso de um tipo condicional dentro de um tipo mapeado. Para cada propriedade K
, ele verifica se o tipo de Configuracao[K]
é uma string. Se for, o tipo é mapeado para number
; caso contrário, permanece inalterado.
4. Remapeamento de Chaves (desde TypeScript 4.1)
O TypeScript 4.1 introduziu a capacidade de remapear chaves em tipos mapeados usando a palavra-chave as
. Isso permite criar novos tipos com nomes de propriedade diferentes com base no tipo original.
interface Evento {
eventId: string;
eventName: string;
eventDate: Date;
}
type EventoTransformado = {
[K em chaveDeEvento as `new${Capitalize<string&K>}`]: Evento[K];
};
// Resultado:
// {
// newEventId: string;
// newEventName: string;
// newEventDate: Date;
// }
//Função Capitalize usada para Capitalizar a primeira letra
type Capitalize<S estende string> = Uppercase<string&S> estende never ? string : `$Capitalize<S>`;
//Uso com um objeto real
const meuEvento: EventoTransformado = {
newEventId: "123",
newEventName: "Novo Nome",
newEventDate: new Date()
};
Aqui, o tipo EventoTransformado
remapeia cada chave K
para uma nova chave prefixada com "new" e capitalizada. A função utilitária `Capitalize` garante que a primeira letra da chave seja capitalizada. A interseção `string & K` garante que estamos lidando apenas com chaves de string e que estamos obtendo o tipo literal correto de K.
O remapeamento de chaves abre possibilidades poderosas para transformar e adaptar tipos a necessidades específicas. Isso permite renomear, filtrar ou modificar chaves com base em lógica complexa.
Principais Conclusões sobre Tipos Mapeados
- Permitem transformar tipos existentes mapeando suas propriedades.
- Tornam propriedades opcionais, somente leitura ou alteram seus tipos.
- São úteis para criar novos tipos com base nas propriedades de outro tipo.
- O remapeamento de chaves (introduzido no TypeScript 4.1) oferece ainda mais flexibilidade nas transformações de tipo.
Combinando Tipos Condicionais e Mapeados
O verdadeiro poder dos tipos condicionais e mapeados surge quando você os combina. Isso permite criar definições de tipo altamente flexíveis e expressivas que podem se adaptar a uma ampla gama de cenários.Exemplo: Filtrando Propriedades por Tipo
Vamos supor que você queira criar um tipo que filtre as propriedades de um objeto com base em seu tipo. Por exemplo, você pode querer extrair apenas as propriedades de string de um objeto.
interface Dados {
name: string;
age: number;
city: string;
country: string;
isEmployed: boolean;
}
type PropriedadesString<T> = {
[K em chaveDeT as T[K] estende string ? K : never]: T[K];
};
type DadosString = PropriedadesString<Dados>;
// Resultado:
// {
// name: string;
// city: string;
// country: string;
// }
const dadosString: DadosString = {
name: "John",
city: "New York",
country: "USA",
};
Neste exemplo, o tipo PropriedadesString<T>
usa um tipo mapeado com remapeamento de chaves e um tipo condicional. Para cada propriedade K
, ele verifica se o tipo de T[K]
é uma string. Se for, a chave é mantida; caso contrário, ela é mapeada para never
, efetivamente filtrando-a. `never` como chave de tipo mapeado a remove do tipo resultante. Isso garante que apenas propriedades de string sejam incluídas no tipo DadosString
.
Tipos Utilitários no TypeScript
O TypeScript fornece vários tipos utilitários embutidos que alavancam tipos condicionais e mapeados para realizar transformações de tipo comuns. Compreender esses tipos utilitários pode simplificar significativamente seu código e melhorar a segurança de tipos.
Tipos Utilitários Comuns
Partial<T>
: Torna todas as propriedades deT
opcionais.Readonly<T>
: Torna todas as propriedades deT
somente leitura.Required<T>
: Torna todas as propriedades deT
obrigatórias. (remove o modificador?
)Pick<T, K estende chaveDeT>
: Seleciona um conjunto de propriedadesK
deT
.Omit<T, K estende chaveDeT>
: Remove um conjunto de propriedadesK
deT
.Record<K estende chaveDeQualquerCoisa, T>
: Constrói um tipo com um conjunto de propriedadesK
do tipoT
.Exclude<T, U>
: Exclui deT
todos os tipos que são atribuíveis aU
.Extract<T, U>
: Extrai deT
todos os tipos que são atribuíveis aU
.NonNullable<T>
: Excluinull
eundefined
deT
.Parameters<T>
: Obtém os parâmetros de um tipo de funçãoT
em uma tupla.ReturnType<T>
: Obtém o tipo de retorno de um tipo de funçãoT
.InstanceType<T>
: Obtém o tipo de instância de um tipo de função construtoraT
.ThisType<T>
: Serve como um marcador para o tipothis
contextual.
Esses tipos utilitários são construídos usando tipos condicionais e mapeados, demonstrando o poder e a flexibilidade desses recursos avançados do TypeScript. Por exemplo, Partial<T>
é definido como:
type Partial<T> = {
[P em chaveDeT]?: T[P];
};
Melhores Práticas para Usar Tipos Condicionais e Mapeados
Embora os tipos condicionais e mapeados sejam poderosos, eles também podem tornar seu código mais complexo se não forem usados com cuidado. Aqui estão algumas melhores práticas a serem consideradas:
- Mantenha Simples: Evite tipos condicionais e mapeados excessivamente complexos. Se a definição de um tipo se tornar muito confusa, considere dividi-la em partes menores e mais gerenciáveis.
- Use Nomes Significativos: Dê aos seus tipos condicionais e mapeados nomes descritivos que indiquem claramente seu propósito.
- Documente Seus Tipos: Adicione comentários para explicar a lógica por trás de seus tipos condicionais e mapeados, especialmente se eles forem complexos.
- Aproveite os Tipos Utilitários: Antes de criar um tipo condicional ou mapeado personalizado, verifique se um tipo utilitário embutido pode alcançar o mesmo resultado.
- Teste Seus Tipos: Certifique-se de que seus tipos condicionais e mapeados se comportem como esperado, escrevendo testes unitários que cubram diferentes cenários.
- Considere o Desempenho: Computações de tipo complexas podem impactar os tempos de compilação. Esteja ciente das implicações de desempenho de suas definições de tipo.
Conclusão
Tipos condicionais e mapeados são ferramentas essenciais para dominar o TypeScript. Eles permitem que você crie aplicações altamente flexíveis, seguras em tipos e fáceis de manter que se adaptam a estruturas de dados complexas e requisitos dinâmicos. Ao compreender e aplicar os conceitos discutidos neste guia, você pode desbloquear todo o potencial do TypeScript e escrever código mais robusto e escalável. À medida que você continua a explorar o TypeScript, lembre-se de experimentar diferentes combinações de tipos condicionais e mapeados para descobrir novas maneiras de resolver problemas de tipagem desafiadores. As possibilidades são verdadeiramente infinitas.