Index signatures e mapped types combinados
1. Fundamentos das Index Signatures
Index signatures permitem definir tipos para objetos com chaves dinâmicas. A sintaxe básica utiliza colchetes para declarar o tipo da chave e o tipo do valor correspondente:
// Sintaxe básica
interface Dictionary {
[key: string]: unknown;
}
// Restrições de tipo para chave (string, number, symbol)
interface NumberMap {
[key: number]: string;
}
const map: NumberMap = {
0: "zero",
1: "um",
2: "dois"
};
// Index signatures com tipos union
interface ConfigMap {
[key: string]: string | number | boolean;
}
// Tipos literais como chave
interface EventMap {
[key: `on${string}`]: () => void;
}
2. Mapped Types: O Poder da Transformação
Mapped types permitem transformar tipos existentes iterando sobre suas propriedades:
// Sintaxe fundamental
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
interface User {
name: string;
age: number;
email: string;
}
type NullableUser = Nullable<User>;
// { name: string | null; age: number | null; email: string | null; }
// Mapped types com genéricos e restrições
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Partial<T> = {
[K in keyof T]?: T[K];
};
3. Combinando Index Signatures com Mapped Types
A verdadeira potência surge quando combinamos index signatures com mapped types para criar tipos dinâmicos complexos:
// Record<K, V> como caso especial
type MyRecord<K extends string | number | symbol, V> = {
[P in K]: V;
};
type StringKeys = MyRecord<"a" | "b" | "c", number>;
// { a: number; b: number; c: number; }
// Index signatures condicionais
type ConditionalMap<T> = {
[K in keyof T]: T[K] extends Function ? T[K] : never;
};
interface Service {
start(): void;
stop(): void;
status: string;
}
type ServiceMethods = ConditionalMap<Service>;
// { start: () => void; stop: () => void; status: never; }
4. Manipulação de Chaves com Template Literals
Template literals com as permitem renomear chaves durante o mapeamento:
// Renomeando chaves com prefixo
type WithPrefix<T, Prefix extends string> = {
[K in keyof T as `${Prefix}${Capitalize<string & K>}`]: T[K];
};
interface Actions {
click: () => void;
hover: () => void;
}
type EventActions = WithPrefix<Actions, "on">;
// { onClick: () => void; onHover: () => void; }
// Sistema de eventos com prefixo
type EventHandler<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (payload: T[K]) => void;
};
interface Events {
login: { userId: string };
logout: void;
error: { message: string; code: number };
}
type EventHandlers = EventHandler<Events>;
// { onLogin: (payload: { userId: string }) => void; ... }
5. Filtragem e Transformação Condicional
Podemos filtrar propriedades usando as never e combinar com tipos condicionais:
// Filtrando propriedades por tipo
type FilterByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
interface Product {
id: number;
name: string;
price: number;
description: string;
category: string;
}
type StringProperties = FilterByType<Product, string>;
// { name: string; description: string; category: string; }
// Combinando com Pick, Omit e Extract
type PublicAPI<T> = {
[K in keyof T as K extends `_${string}` ? never : K]: T[K];
};
interface InternalAPI {
_secret: string;
_config: object;
publicMethod(): void;
data: unknown;
}
type Public = PublicAPI<InternalAPI>;
// { publicMethod(): void; data: unknown; }
6. Casos Avançados: Deep Mapped Types
Mapped types recursivos permitem transformar objetos aninhados:
// DeepReadonly recursivo
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
logging: {
level: string;
transports: string[];
};
}
type ImmutableConfig = DeepReadonly<Config>;
// Todas as propriedades são readonly, inclusive aninhadas
// DeepPartial recursivo
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends Function
? T[K]
: DeepPartial<T[K]>
: T[K];
};
type PartialConfig = DeepPartial<Config>;
// Todas as propriedades são opcionais, inclusive aninhadas
7. Validação e Segurança com Constraint
Restringindo chaves e validando tipos de valor para maior segurança:
// Restringindo chaves com extends keyof any
type SafeDictionary<TKey extends string, TValue> = {
[K in TKey]: TValue;
};
type AllowedKeys = "name" | "age" | "email";
type UserDict = SafeDictionary<AllowedKeys, string>;
// { name: string; age: string; email: string; }
// Dictionary com validação de tipo de valor
type TypedDictionary<TKey extends string | number | symbol, TValue> = {
[K in TKey]: TValue extends object
? { readonly [P in keyof TValue]: TValue[P] }
: TValue;
};
interface Settings {
theme: "light" | "dark";
language: string;
notifications: boolean;
}
type ImmutableSettings = TypedDictionary<keyof Settings, Settings[keyof Settings]>;
// { theme: { readonly theme: "light" | "dark" }; language: { readonly language: string }; ... }
8. Padrões Práticos e Boas Práticas
Exemplos reais combinando todas as técnicas:
// Sistema de configuração tipado com fallback
type ConfigSchema = {
api: {
baseUrl: string;
timeout: number;
retries: number;
};
features: {
darkMode: boolean;
analytics: boolean;
};
};
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
type UserConfig = DeepPartial<ConfigSchema>;
// Função que mescla configuração com defaults
function mergeConfig<T extends object>(
defaults: T,
overrides: DeepPartial<T>
): T {
const result = { ...defaults };
for (const key in overrides) {
if (overrides[key] && typeof overrides[key] === 'object') {
result[key] = mergeConfig(defaults[key], overrides[key]);
} else if (overrides[key] !== undefined) {
result[key] = overrides[key] as any;
}
}
return result;
}
// Uso com as const e satisfies
const defaultConfig = {
api: {
baseUrl: "https://api.example.com",
timeout: 5000,
retries: 3
},
features: {
darkMode: false,
analytics: true
}
} as const satisfies ConfigSchema;
const userOverrides = {
api: {
timeout: 10000
}
} satisfies DeepPartial<ConfigSchema>;
const finalConfig = mergeConfig(defaultConfig, userOverrides);
Boas práticas:
- Evite index signatures quando as chaves são conhecidas em tempo de compilação
- Prefira mapped types para transformações previsíveis
- Use as const para inferir tipos literais corretamente
- Combine satisfies para validar sem alterar o tipo inferido
- Documente tipos complexos com comentários JSDoc
Referências
- TypeScript Handbook: Index Signatures — Documentação oficial sobre index signatures, sintaxe e restrições
- TypeScript Handbook: Mapped Types — Guia completo sobre mapped types, incluindo template literals e key remapping
- TypeScript Deep Dive: Index Signatures — Tutorial aprofundado sobre index signatures com exemplos práticos
- TypeScript 4.1: Template Literal Types — Documentação oficial sobre template literal types e key remapping com
as - TypeScript Playground: Mapped Types Examples — Exemplos interativos oficiais de mapped types no TypeScript Playground
- TypeScript Deep Dive: Mapped Types — Guia detalhado sobre mapped types com casos de uso avançados