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:
- 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!
};
- 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
- TypeScript Handbook: Mapped Types — Documentação oficial sobre mapped types, incluindo key remapping com a cláusula
as - TypeScript 4.1 Release Notes: Key Remapping — Notas oficiais de lançamento do TypeScript 4.1 com detalhes sobre key remapping
- Template Literal Types in TypeScript — Documentação oficial sobre template literal types, essenciais para transformações avançadas de chaves
- TypeScript Deep Dive: Mapped Types — Guia aprofundado sobre mapped types com exemplos práticos de key remapping
- Understanding TypeScript's Key Remapping — Artigo técnico no Dev.to com exemplos detalhados de filtragem e transformação de chaves