Key remapping em mapped types

1. Introdução aos Mapped Types e a Necessidade de Remapeamento

Mapped types são um dos recursos mais poderosos do sistema de tipos do TypeScript. A sintaxe básica { [P in K]: T } permite transformar um conjunto de chaves em um novo tipo, onde cada propriedade pode ter seu tipo modificado. Por exemplo:

type Point = { x: number; y: number };
type ReadonlyPoint = { readonly [P in keyof Point]: Point[P] };
// Resultado: { readonly x: number; readonly y: number }

No entanto, os mapped types tradicionais têm uma limitação significativa: as chaves resultantes são sempre idênticas ao conjunto original. Não é possível renomear, filtrar ou transformar os nomes das propriedades durante o mapeamento.

A motivação para o key remapping surge justamente dessa necessidade: em cenários reais, frequentemente precisamos criar tipos derivados onde os nomes das chaves são transformados — como prefixar chaves com get, converter snake_case para camelCase, ou excluir propriedades privadas.

2. Sintaxe Básica de Key Remapping com as

O TypeScript 4.1 introduziu a cláusula as em mapped types, permitindo redefinir o nome de cada chave durante o mapeamento. A sintaxe é:

{ [P in K as NovoNome]: T }

O exemplo mais simples é prefixar todas as chaves com get:

type Person = {
  name: string;
  age: number;
};

type Getters = {
  [K in keyof Person as `get${Capitalize<string & K>}`]: () => Person[K]
};
// Resultado: { getName: () => string; getAge: () => number }

O TypeScript fornece tipos utilitários intrínsecos para manipulação de strings: Capitalize, Uncapitalize, Uppercase, Lowercase:

type EventName = "click" | "mouseenter" | "mouseleave";
type ListenerNames = {
  [E in EventName as `on${Capitalize<E>}`]: () => void
};
// Resultado: { onClick: () => void; onMouseenter: () => void; onMouseleave: () => void }

3. Filtragem de Chaves com Key Remapping

Uma aplicação poderosa do key remapping é filtrar chaves usando o tipo never. Quando uma chave é mapeada para never, ela é removida do tipo resultante:

type User = {
  id: number;
  name: string;
  _password: string;
  _token: string;
};

type PublicUser = {
  [K in keyof User as K extends `_${string}` ? never : K]: User[K]
};
// Resultado: { id: number; name: string }

Podemos combinar com tipos condicionais para criar filtros mais sofisticados:

type ExcludeByValueType<T, V> = {
  [K in keyof T as T[K] extends V ? never : K]: T[K]
};

type StringFreeUser = ExcludeByValueType<User, string>;
// Resultado: { id: number }

4. Transformação Avançada de Chaves com Template Literal Types

Os template literal types dentro do as permitem transformações complexas nos nomes das chaves. Um caso clássico é converter snake_case para camelCase:

type SnakeToCamel<S extends string> = 
  S extends `${infer First}_${infer Rest}` 
    ? `${First}${Capitalize<SnakeToCamel<Rest>>}` 
    : S;

type ApiResponse = {
  user_id: number;
  user_name: string;
  created_at: Date;
};

type CamelCaseResponse = {
  [K in keyof ApiResponse as SnakeToCamel<K>]: ApiResponse[K]
};
// Resultado: { userId: number; userName: string; createdAt: Date }

Podemos também adicionar prefixos e sufixos dinâmicos baseados no tipo do valor:

type AddSuffixByType<T> = {
  [K in keyof T as T[K] extends string 
    ? `${string & K}_text` 
    : T[K] extends number 
      ? `${string & K}_number` 
      : K
  ]: T[K]
};

type Config = { name: string; count: number; enabled: boolean };
type SuffixedConfig = AddSuffixByType<Config>;
// Resultado: { name_text: string; count_number: number; enabled: boolean }

5. Combinação com Tipos Genéricos e Inferência

Podemos criar tipos genéricos reutilizáveis que aceitam mapeadores de chave personalizados:

type MapKeys<T, Mapper extends (key: keyof T) => string> = {
  [K in keyof T as Mapper(K)]: T[K]
};

type PrefixWithGet<T> = MapKeys<T, (k: keyof T) => `get${Capitalize<string & k>}`>;

type Person = { name: string; age: number };
type PersonGetters = PrefixWithGet<Person>;
// Resultado: { getName: string; getAge: number }

Usando infer para extrair padrões e reestruturar chaves:

type PickByPrefix<T, Prefix extends string> = {
  [K in keyof T as K extends `${Prefix}${infer Rest}` ? Uncapitalize<Rest> : never]: T[K]
};

type Events = {
  onClick: () => void;
  onMouseEnter: () => void;
  onMouseLeave: () => void;
  version: string;
};

type ClickEvents = PickByPrefix<Events, "on">;
// Resultado: { click: () => void; mouseEnter: () => void; mouseLeave: () => void }

6. Casos de Uso Práticos e Padrões Comuns

Criando getters tipados a partir de interfaces:

interface UserData {
  id: number;
  email: string;
  role: "admin" | "user";
}

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

type UserGetters = Getters<UserData>;
// { getId: () => number; getEmail: () => string; getRole: () => "admin" | "user" }

Transformação de APIs REST para formatos internos:

type ApiUser = {
  first_name: string;
  last_name: string;
  date_of_birth: string;
};

type InternalUser = {
  [K in keyof ApiUser as SnakeToCamel<K>]: ApiUser[K]
};
// { firstName: string; lastName: string; dateOfBirth: string }

Mapeamento de eventos para handlers tipados:

type AppEvents = {
  user_login: { userId: number };
  user_logout: { userId: number };
  error_occurred: { message: string; code: number };
};

type EventHandlers = {
  [K in keyof AppEvents as `on${Capitalize<SnakeToCamel<K>>}`]: (data: AppEvents[K]) => void
};
// { onUserLogin: (data: { userId: number }) => void; ... }

7. Limitações e Boas Práticas

Limitações importantes:

  1. Sem acesso ao tipo do valor para modificar a chave: O remapeamento opera apenas sobre o nome da chave, não sobre seu tipo associado:
// Isto NÃO é possível:
type Invalid = {
  [K in keyof T as T[K] extends string ? `str_${K}` : K]: T[K] // Erro!
};
  1. Chaves duplicadas não são prevenidas: Se o remapeamento gerar chaves iguais, o TypeScript não emitirá erro:
type Duplicate = {
  [K in "a" | "b" as "same"]: K
};
// Resultado: { same: "a" | "b" } — sem erro de duplicata

Boas práticas:

  • Prefira utility types prontos (Pick, Omit, Record) quando possível
  • Use key remapping para transformações que não podem ser expressas com tipos utilitários padrão
  • Documente tipos genéricos complexos com exemplos de uso
  • Evite aninhamento excessivo de template literal types

8. Exemplo Completo: Sistema de Formulários Tipados

Vamos construir um sistema completo de formulários usando key remapping:

// Tipo base de campos do formulário
type FormFields = {
  username: string;
  password: string;
  email: string;
  remember_me: boolean;
};

// Estado do formulário (todos os campos tornam-se opcionais)
type FormState = {
  [K in keyof FormFields as `state_${string & K}`]: FormFields[K] | undefined
};

// Tipo de erros de validação
type FormErrors = {
  [K in keyof FormFields as `error_${string & K}`]: string | null
};

// Tipo de touched (campos que foram interagidos)
type FormTouched = {
  [K in keyof FormFields as `touched_${string & K}`]: boolean
};

// Tipo completo do formulário combinando todos os aspectos
type CompleteForm<T> = {
  [K in keyof T as `value_${string & K}`]: T[K]
} & {
  [K in keyof T as `error_${string & K}`]: string | null
} & {
  [K in keyof T as `touched_${string & K}`]: boolean
};

// Gerando tipos de validação com filtragem
type ValidatableFields<T> = {
  [K in keyof T as T[K] extends boolean ? never : K]: T[K]
};

type NonBooleanFields = ValidatableFields<FormFields>;
// { username: string; password: string; email: string }

// Função de validação tipada
type ValidationResult<T> = {
  [K in keyof T as `valid_${string & K}`]: boolean
};

type FormValidation = ValidationResult<NonBooleanFields>;
// { valid_username: boolean; valid_password: boolean; valid_email: boolean }

// Uso prático
const form: CompleteForm<FormFields> = {
  value_username: "john_doe",
  value_password: "secret123",
  value_email: "john@example.com",
  value_remember_me: true,
  error_username: null,
  error_password: "Too short",
  error_email: null,
  error_remember_me: null,
  touched_username: true,
  touched_password: true,
  touched_email: false,
  touched_remember_me: false
};

Este exemplo demonstra como key remapping permite criar um sistema de formulários completamente tipado, com separação clara entre valores, erros e estados de interação — tudo gerado automaticamente a partir de uma definição base de campos.

Referências