Português

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;
};

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

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.

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

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.