Dicionário TypeScript: Guia Completo para Objetos com Segurança de Tipos

Ao trabalhar com pares chave-valor em TypeScript, você precisa de uma maneira confiável para manter a segurança de tipos ao acessar e manipular dados. Diferentemente de linguagens com tipos de dicionário integrados, o TypeScript requer abordagens específicas para criar dicionários com segurança de tipos. Este guia explora todas as opções de implementação, com exemplos práticos para ajudá-lo a escolher a abordagem certa para seus projetos.
Principais Pontos
- O TypeScript oferece múltiplas formas de implementar dicionários: assinaturas de índice, utilitário
Record
eMap
- Escolha a implementação com base em suas necessidades: assinaturas de índice para dicionários simples,
Record
para conjuntos específicos de chaves eMap
para recursos avançados - Aproveite o sistema de tipos do TypeScript para garantir segurança em tempo de compilação para operações de dicionário
- Considere as implicações de desempenho ao selecionar entre dicionários baseados em objetos e Map
- Use técnicas avançadas de tipos como tipos mapeados para requisitos complexos de dicionário
O que é um Dicionário TypeScript?
Um dicionário em TypeScript é uma estrutura de dados que armazena pares chave-valor, permitindo consultas eficientes por chave. Embora o TypeScript não tenha um tipo Dictionary
nativo, ele fornece várias maneiras de implementar estruturas semelhantes a dicionários com total segurança de tipos.
Um dicionário com segurança de tipos garante:
- Chaves e valores em conformidade com tipos específicos
- Erros em tempo de compilação para incompatibilidades de tipos
- Suporte a autocompletar e IntelliSense
- Proteção contra erros em tempo de execução
Opções de Implementação de Dicionário em TypeScript
Usando Assinaturas de Índice
A abordagem mais direta usa objetos JavaScript com assinaturas de índice TypeScript:
// Dicionário básico com assinatura de índice
const userScores: { [username: string]: number } = {};
// Adicionando entradas
userScores[""alice""] = 95;
userScores[""bob""] = 87;
// Acessando valores
console.log(userScores[""alice""]); // 95
// Segurança de tipos em ação
userScores[""charlie""] = ""high""; // Erro: Type 'string' is not assignable to type 'number'
Esta abordagem é simples, mas torna-se complicada quando reutilizada em todo o seu código.
Criando um Tipo de Dicionário Reutilizável
Para melhor reutilização, defina um tipo de dicionário genérico:
// Tipo de dicionário genérico
type Dictionary<K extends string | number | symbol, V> = {
[key in K]: V;
};
// Uso com chaves string e valores numéricos
const scores: Dictionary<string, number> = {};
scores[""math""] = 95;
scores[""science""] = 87;
// Uso com chaves numéricas
const idMapping: Dictionary<number, string> = {};
idMapping[1] = ""user_a"";
idMapping[2] = ""user_b"";
Usando o Tipo Utilitário Record
O tipo utilitário Record
integrado do TypeScript fornece uma sintaxe mais limpa:
// Usando Record para conjunto fixo de chaves
type UserFields = ""name"" | ""email"" | ""role"";
const user: Record<UserFields, string> = {
name: ""Alice Smith"",
email: ""alice@example.com"",
role: ""Admin""
};
// Usando Record com quaisquer chaves string
const config: Record<string, any> = {};
config[""apiUrl""] = ""https://api.example.com"";
config[""timeout""] = 5000;
O tipo Record
é especialmente útil quando você precisa impor um conjunto específico de chaves usando tipos literais de string.
Usando Map do JavaScript com TypeScript
Para recursos mais avançados, use o objeto Map
do JavaScript com genéricos TypeScript:
// Map com segurança de tipos
const userProfiles = new Map<string, {age: number, active: boolean}>();
// Adicionando entradas
userProfiles.set(""alice"", {age: 28, active: true});
userProfiles.set(""bob"", {age: 34, active: false});
// Verificação de tipos funciona
userProfiles.set(""charlie"", {age: ""thirty""}); // Erro: Type 'string' not assignable to type 'number'
// Acessando valores
const aliceProfile = userProfiles.get(""alice"");
console.log(aliceProfile?.age); // 28
Comparando Implementações de Dicionário
Recurso Objeto com Assinatura de Índice Tipo Record Map Tipos de chave String, number, symbol Qualquer tipo Qualquer tipo Desempenho Rápido para conjuntos pequenos Rápido para conjuntos pequenos Melhor para adições/exclusões frequentes Uso de memória Menor Menor Maior Preservação de ordem Não Não Sim (ordem de inserção) Iteração Requer Object.entries()
Requer Object.entries()
Iteradores integrados Métodos especiais Não Não has()
, delete()
, clear()
Risco de poluição de protótipo Sim Sim Não
Operações Comuns de Dicionário
Verificando se uma Chave Existe
// Usando assinatura de índice
const hasKey = (dict: Record<string, unknown>, key: string): boolean => {
return key in dict;
};
// Usando Map
const userMap = new Map<string, number>();
userMap.has(""alice""); // true/false
Adicionando e Atualizando Valores
// Usando assinatura de índice
type UserDict = Record<string, {name: string, role: string}>;
const users: UserDict = {};
// Adicionando
users[""u1""] = {name: ""Alice"", role: ""Admin""};
// Atualizando
users[""u1""] = {...users[""u1""], role: ""User""};
// Usando Map
const userMap = new Map<string, {name: string, role: string}>();
userMap.set(""u1"", {name: ""Alice"", role: ""Admin""});
userMap.set(""u1"", {name: ""Alice"", role: ""User""}); // Atualiza entrada existente
Excluindo Chaves
// Usando assinatura de índice
delete users[""u1""];
// Usando Map
userMap.delete(""u1"");
Iterando por Dicionários
// Iteração em dicionário baseado em objeto
const scores: Record<string, number> = {math: 95, science: 87};
// Método 1: Object.entries()
for (const [subject, score] of Object.entries(scores)) {
console.log(`${subject}: ${score}`);
}
// Método 2: Object.keys()
Object.keys(scores).forEach(subject => {
console.log(`${subject}: ${scores[subject]}`);
});
// Iteração de Map
const scoreMap = new Map<string, number>([
[""math"", 95],
[""science"", 87]
]);
// Iteração direta
for (const [subject, score] of scoreMap) {
console.log(`${subject}: ${score}`);
}
// Usando forEach
scoreMap.forEach((score, subject) => {
console.log(`${subject}: ${score}`);
});
Técnicas Avançadas de Segurança de Tipos
Restringindo Tipos de Chave
// Usando união de tipos literais de string
type ValidKeys = ""id"" | ""name"" | ""email"";
const userData: Record<ValidKeys, string> = {
id: ""123"",
name: ""Alice"",
email: ""alice@example.com""
};
// Tentando usar chaves inválidas
userData.phone = ""555-1234""; // Erro: Property 'phone' does not exist
Dicionários Somente Leitura
// Dicionário imutável
const constants: Readonly<Record<string, number>> = {
MAX_USERS: 100,
TIMEOUT_MS: 5000
};
constants.MAX_USERS = 200; // Erro: Cannot assign to 'MAX_USERS' because it is a read-only property
Dicionários Parciais
// Dicionário com valores opcionais
type UserProfile = {
name: string;
email: string;
age: number;
};
// Criar um dicionário onde cada valor pode ter algumas ou todas as propriedades de UserProfile
const profiles: Record<string, Partial<UserProfile>> = {};
profiles[""alice""] = { name: ""Alice"" }; // Válido mesmo sem email e age
profiles[""bob""] = { name: ""Bob"", email: ""bob@example.com"" }; // Válido
Usando Tipos Mapeados para Dicionários Avançados
type SensorData = {
temperature: number;
humidity: number;
pressure: string;
metadata: object;
};
// Criar um tipo de dicionário que inclui apenas propriedades numéricas de SensorData
type NumericSensorDict = {
[K in keyof SensorData as SensorData[K] extends number ? K : never]: boolean;
};
// O tipo resultante tem apenas temperature e humidity como chaves válidas
const sensorStatus: NumericSensorDict = {
temperature: true,
humidity: false
};
Considerações de Desempenho
Ao escolher uma implementação de dicionário, considere:
- Tamanho dos dados: Para conjuntos pequenos com chaves string, dicionários baseados em objetos são eficientes
- Modificações frequentes:
Map
tem melhor desempenho para adições e exclusões frequentes - Uso de memória: Dicionários baseados em objetos usam menos memória que
Map
- Tipos de chave: Se você precisa de chaves não-string, use
Map
Resultados de benchmark para 10.000 operações:
Acesso a Objeto: ~25ms
Acesso a Map: ~45ms
Inserção em Objeto: ~30ms
Inserção em Map: ~50ms
Exclusão em Objeto: ~20ms
Exclusão em Map: ~15ms
Casos de Uso do Mundo Real
Gerenciamento de Configuração
type AppConfig = Record<string, string | number | boolean>;
const config: AppConfig = {
apiUrl: ""https://api.example.com"",
timeout: 5000,
enableCache: true
};
Implementação de Cache
class Cache<T> {
private store = new Map<string, {value: T, expiry: number}>();
set(key: string, value: T, ttlSeconds: number): void {
const expiry = Date.now() + (ttlSeconds * 1000);
this.store.set(key, {value, expiry});
}
get(key: string): T | null {
const item = this.store.get(key);
if (!item) return null;
if (item.expiry < Date.now()) {
this.store.delete(key);
return null;
}
return item.value;
}
}
const userCache = new Cache<{name: string, role: string}>();
userCache.set(""user:123"", {name: ""Alice"", role: ""Admin""}, 300);
Gerenciamento de Estado no React
interface UserState {
users: Record<string, {name: string, role: string}>;
loading: boolean;
error: string | null;
}
const initialState: UserState = {
users: {},
loading: false,
error: null
};
// Em um reducer
function userReducer(state = initialState, action: any): UserState {
switch (action.type) {
case 'ADD_USER':
return {
...state,
users: {
...state.users,
[action.payload.id]: {
name: action.payload.name,
role: action.payload.role
}
}
};
// Outros casos...
default:
return state;
}
}
Estratégias de Tratamento de Erros
Lidando com Chaves Ausentes
function safeGet<K extends string, V>(
dict: Record<K, V>,
key: string,
defaultValue: V
): V {
return (key in dict) ? dict[key as K] : defaultValue;
}
const users: Record<string, string> = {
""1"": ""Alice""
};
const userName = safeGet(users, ""2"", ""Unknown User"");
console.log(userName); // ""Unknown User""
Guardas de Tipo para Valores de Dicionário
function isUserDict(obj: unknown): obj is Record<string, {name: string, age: number}> {
if (typeof obj !== 'object' || obj === null) return false;
for (const key in obj as object) {
const value = (obj as any)[key];
if (typeof value !== 'object' ||
typeof value.name !== 'string' ||
typeof value.age !== 'number') {
return false;
}
}
return true;
}
// Uso
function processUserData(data: unknown) {
if (isUserDict(data)) {
// TypeScript sabe que data é Record<string, {name: string, age: number}>
for (const [id, user] of Object.entries(data)) {
console.log(`User ${id}: ${user.name}, ${user.age}`);
}
}
}
Conclusão
Os dicionários TypeScript fornecem maneiras poderosas de gerenciar dados de chave-valor com total segurança de tipos. Ao escolher a abordagem de implementação certa com base em seus requisitos específicos, você pode construir aplicações mais robustas com menos erros em tempo de execução. Seja você precisando de objetos simples com chaves string ou dicionários complexos com recursos avançados, o sistema de tipos do TypeScript garante que suas operações de dicionário permaneçam seguras em todo o seu código.
Perguntas Frequentes
Use Map quando precisar de chaves não-string, garantia de preservação da ordem das chaves, adições e exclusões frequentes, ou proteção contra poluição de protótipo. Maps fornecem métodos integrados como has(), delete() e clear() que tornam certas operações mais convenientes
Use tipos literais de string com o tipo utilitário Record:nn`typescriptntype AllowedKeys = 'id' | 'name' | 'email';nconst user: Record<AllowedKeys, string> = {n id: '123',n name: 'Alice',n email: 'alice@example.com'n};n`nnIsso garante que o TypeScript mostrará um erro se você tentar usar chaves que não estão na união AllowedKeys
Não com dicionários baseados em objetos, mas você pode com Map:nn`typescriptnconst userMap = new Map<{id: number}, string>();nconst key1 = {id: 1};nconst key2 = {id: 1}; // Referência de objeto diferentennuserMap.set(key1, 'Alice');nconsole.log(userMap.get(key1)); // 'Alice'nconsole.log(userMap.get(key2)); // undefined (referência de objeto diferente)n`nnObserve que Map usa igualdade de referência para chaves de objeto
Converter objeto para Map:nn`typescriptnconst obj: Record<string, number> = {a: 1, b: 2};nconst map = new Map(Object.entries(obj));n`nnConverter Map para objeto:nn`typescriptnconst mapInstance = new Map<string, number>([['a', 1], ['b', 2]]);nconst objFromMap = Object.fromEntries(mapInstance);n`
Use encadeamento opcional e coalescência nula:nn`typescriptnconst users: Record<string, {name: string} | undefined> = {n '1': {name: 'Alice'}n};nn// Acesso seguro com fallbacknconst userName = users['2']?.name ?? 'Unknown';nconsole.log(userName); // 'Unknown'n`nnIsso evita erros em tempo de execução ao acessar propriedades de valores potencialmente indefinidos