Um guia completo sobre os poderosos Mapped Types e Conditional Types do TypeScript, com exemplos práticos para criar aplicações robustas e seguras.
Dominando Mapped Types e Conditional Types do TypeScript
O TypeScript, um superconjunto do JavaScript, oferece recursos poderosos para a criação de aplicações robustas e de fácil manutenção. Entre esses recursos, os Mapped Types (Tipos Mapeados) e Conditional Types (Tipos Condicionais) destacam-se como ferramentas essenciais para a manipulação avançada de tipos. Este guia oferece uma visão abrangente desses conceitos, explorando sua sintaxe, aplicações práticas e casos de uso avançados. Seja você um desenvolvedor experiente em TypeScript ou apenas começando sua jornada, este artigo irá equipá-lo com o conhecimento para aproveitar esses recursos de forma eficaz.
O que são Mapped Types?
Mapped Types permitem que você crie novos tipos transformando tipos existentes. Eles iteram sobre as propriedades de um tipo existente e aplicam uma transformação a cada propriedade. Isso é particularmente útil para criar variações de tipos existentes, como tornar todas as propriedades opcionais ou somente leitura.
Sintaxe Básica
A sintaxe para um Mapped Type é a seguinte:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: O tipo de entrada sobre o qual você deseja mapear.K in keyof T
: Itera sobre cada chave no tipo de entradaT
.keyof T
cria uma união de todos os nomes de propriedade emT
, eK
representa cada chave individual durante a iteração.Transformation
: A transformação que você deseja aplicar a cada propriedade. Isso pode ser adicionar um modificador (comoreadonly
ou?
), alterar o tipo ou outra coisa completamente.
Exemplos Práticos
Tornando Propriedades Somente Leitura
Digamos que você tenha uma interface que representa um perfil de usuário:
interface UserProfile {
name: string;
age: number;
email: string;
}
Você pode criar um novo tipo onde todas as propriedades são somente leitura:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
Agora, ReadOnlyUserProfile
terá as mesmas propriedades que UserProfile
, mas todas serão somente leitura.
Tornando Propriedades Opcionais
Da mesma forma, você pode tornar todas as propriedades opcionais:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
OptionalUserProfile
terá todas as propriedades de UserProfile
, mas cada propriedade será opcional.
Modificando Tipos de Propriedade
Você também pode modificar o tipo de cada propriedade. Por exemplo, pode transformar todas as propriedades para que sejam strings:
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
Neste caso, todas as propriedades em StringifiedUserProfile
serão do tipo string
.
O que são Conditional Types?
Conditional Types permitem que você defina tipos que dependem de uma condição. Eles fornecem uma maneira de expressar relações de tipo com base em se um tipo satisfaz uma restrição específica. Isso é semelhante a um operador ternário em JavaScript, mas para tipos.
Sintaxe Básica
A sintaxe para um Conditional Type é a seguinte:
T extends U ? X : Y
T
: O tipo que está sendo verificado.U
: O tipo que está sendo estendido porT
(a condição).X
: O tipo a ser retornado seT
estenderU
(a condição é verdadeira).Y
: O tipo a ser retornado seT
não estenderU
(a condição é falsa).
Exemplos Práticos
Determinando se um Tipo é uma String
Vamos criar um tipo que retorna string
se o tipo de entrada for uma string, e number
caso contrário:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
Extraindo um Tipo de uma Union
Você pode usar tipos condicionais para extrair um tipo específico de um tipo de união (union). Por exemplo, para extrair tipos não nulos:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
Aqui, se T
for null
ou undefined
, o tipo se torna never
, que é então filtrado pela simplificação de tipos de união do TypeScript.
Inferindo Tipos
Tipos condicionais também podem ser usados para inferir tipos usando a palavra-chave infer
. Isso permite extrair um tipo de uma estrutura de tipo mais complexa.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type Result5 = ReturnType<typeof myFunction>; // string
Neste exemplo, ReturnType
extrai o tipo de retorno de uma função. Ele verifica se T
é uma função que aceita quaisquer argumentos e retorna um tipo R
. Se for, ele retorna R
; caso contrário, retorna any
.
Combinando Mapped Types e Conditional Types
O verdadeiro poder dos Mapped Types e Conditional Types vem da sua combinação. Isso permite que você crie transformações de tipo altamente flexíveis e expressivas.
Exemplo: Deep Readonly
Um caso de uso comum é criar um tipo que torna todas as propriedades de um objeto, incluindo propriedades aninhadas, somente leitura. Isso pode ser alcançado usando um tipo condicional recursivo.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
Aqui, DeepReadonly
aplica recursivamente o modificador readonly
a todas as propriedades e suas propriedades aninhadas. Se uma propriedade for um objeto, ele chama recursivamente DeepReadonly
nesse objeto. Caso contrário, ele simplesmente aplica o modificador readonly
à propriedade.
Exemplo: Filtrando Propriedades por Tipo
Digamos que você queira criar um tipo que inclua apenas propriedades de um tipo específico. Você pode combinar Mapped Types e Conditional Types para conseguir isso.
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type StringProperties = FilterByType<Person, string>; // { name: string; }
type NonStringProperties = Omit<Person, keyof StringProperties>;
Neste exemplo, FilterByType
itera sobre as propriedades de T
e verifica se o tipo de cada propriedade estende U
. Se estender, ele inclui a propriedade no tipo resultante; caso contrário, ele a exclui mapeando a chave para never
. Note o uso de "as" para remapear chaves. Em seguida, usamos Omit
e keyof StringProperties
para remover as propriedades de string da interface original.
Casos de Uso e Padrões Avançados
Além dos exemplos básicos, Mapped Types e Conditional Types podem ser usados em cenários mais avançados para criar aplicações altamente personalizáveis e com segurança de tipo.
Tipos Condicionais Distributivos
Tipos condicionais são distributivos quando o tipo verificado é um tipo de união. Isso significa que a condição é aplicada a cada membro da união individualmente, e os resultados são então combinados em um novo tipo de união.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
Neste exemplo, ToArray
é aplicado a cada membro da união string | number
individualmente, resultando em string[] | number[]
. Se a condição não fosse distributiva, o resultado teria sido (string | number)[]
.
Usando Tipos Utilitários
O TypeScript fornece vários tipos utilitários integrados que aproveitam Mapped Types e Conditional Types. Esses tipos utilitários podem ser usados como blocos de construção para transformações de tipo mais complexas.
Partial<T>
: Torna todas as propriedades deT
opcionais.Required<T>
: Torna todas as propriedades deT
obrigatórias.Readonly<T>
: Torna todas as propriedades deT
somente leitura.Pick<T, K>
: Seleciona um conjunto de propriedadesK
deT
.Omit<T, K>
: Remove um conjunto de propriedadesK
deT
.Record<K, 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
.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
.
Esses tipos utilitários são ferramentas poderosas que podem simplificar manipulações complexas de tipos. Por exemplo, você pode combinar Pick
e Partial
para criar um tipo que torna apenas certas propriedades opcionais:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type OptionalDescriptionProduct = Optional<Product, "description">;
Neste exemplo, OptionalDescriptionProduct
tem todas as propriedades de Product
, mas a propriedade description
é opcional.
Usando Template Literal Types
Template Literal Types permitem criar tipos baseados em literais de string. Eles podem ser usados em combinação com Mapped Types e Conditional Types para criar transformações de tipo dinâmicas e expressivas. Por exemplo, você pode criar um tipo que prefixa todos os nomes de propriedade com uma string específica:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Settings {
apiUrl: string;
timeout: number;
}
type PrefixedSettings = Prefix<Settings, "data_">;
Neste exemplo, PrefixedSettings
terá as propriedades data_apiUrl
e data_timeout
.
Melhores Práticas e Considerações
- Mantenha a Simplicidade: Embora os Mapped Types e Conditional Types sejam poderosos, eles também podem tornar seu código mais complexo. Tente manter suas transformações de tipo o mais simples possível.
- Use Tipos Utilitários: Aproveite os tipos utilitários integrados do TypeScript sempre que possível. Eles são bem testados e podem simplificar seu código.
- Documente Seus Tipos: Documente claramente suas transformações de tipo, especialmente se forem complexas. Isso ajudará outros desenvolvedores a entender seu código.
- Teste Seus Tipos: Use a verificação de tipos do TypeScript para garantir que suas transformações de tipo estejam funcionando como esperado. Você pode escrever testes unitários para verificar o comportamento de seus tipos.
- Considere o Desempenho: Transformações de tipo complexas podem impactar o desempenho do seu compilador TypeScript. Esteja ciente da complexidade de seus tipos и evite computações desnecessárias.
Conclusão
Mapped Types e Conditional Types são recursos poderosos no TypeScript que permitem criar transformações de tipo altamente flexíveis e expressivas. Ao dominar esses conceitos, você pode melhorar a segurança de tipo, a manutenibilidade e a qualidade geral de suas aplicações TypeScript. De transformações simples, como tornar propriedades opcionais ou somente leitura, a transformações recursivas complexas e lógica condicional, esses recursos fornecem as ferramentas necessárias para construir aplicações robustas e escaláveis. Continue explorando e experimentando com esses recursos para desbloquear todo o seu potencial e se tornar um desenvolvedor TypeScript mais proficiente.
À medida que você continua sua jornada com o TypeScript, lembre-se de aproveitar a riqueza de recursos disponíveis, incluindo a documentação oficial do TypeScript, comunidades online e projetos de código aberto. Abrace o poder dos Mapped Types e Conditional Types, e você estará bem equipado para enfrentar até os problemas mais desafiadores relacionados a tipos.