Back

Diccionario de TypeScript: Guía completa para objetos con seguridad de tipos

Diccionario de TypeScript: Guía completa para objetos con seguridad de tipos

Cuando trabajas con pares clave-valor en TypeScript, necesitas una forma confiable de mantener la seguridad de tipos al acceder y manipular datos. A diferencia de los lenguajes con tipos de diccionario incorporados, TypeScript requiere enfoques específicos para crear diccionarios con seguridad de tipos. Esta guía explora todas las opciones de implementación, con ejemplos prácticos para ayudarte a elegir el enfoque adecuado para tus proyectos.

Puntos clave

  • TypeScript ofrece múltiples formas de implementar diccionarios: firmas de índice, utilidad Record y Map
  • Elige la implementación según tus necesidades: firmas de índice para diccionarios simples, Record para conjuntos específicos de claves y Map para características avanzadas
  • Aprovecha el sistema de tipos de TypeScript para garantizar la seguridad en tiempo de compilación para operaciones de diccionario
  • Considera las implicaciones de rendimiento al seleccionar entre diccionarios basados en objetos y Map
  • Utiliza técnicas de tipos avanzadas como tipos mapeados para requisitos complejos de diccionario

¿Qué es un diccionario en TypeScript?

Un diccionario en TypeScript es una estructura de datos que almacena pares clave-valor, permitiendo búsquedas eficientes por clave. Aunque TypeScript no tiene un tipo Dictionary nativo, proporciona varias formas de implementar estructuras similares a diccionarios con seguridad de tipos completa.

Un diccionario con seguridad de tipos garantiza:

  • Que las claves y valores se ajusten a tipos específicos
  • Errores en tiempo de compilación para discrepancias de tipos
  • Soporte para autocompletado e IntelliSense
  • Protección contra errores en tiempo de ejecución

Opciones de implementación de diccionarios en TypeScript

Uso de firmas de índice

El enfoque más directo utiliza objetos JavaScript con firmas de índice de TypeScript:

// Diccionario básico con firma de índice
const userScores: { [username: string]: number } = {};

// Añadiendo entradas
userScores[""alice""] = 95;
userScores[""bob""] = 87;

// Accediendo a valores
console.log(userScores[""alice""]); // 95

// Seguridad de tipos en acción
userScores[""charlie""] = ""high""; // Error: Type 'string' is not assignable to type 'number'

Este enfoque es simple pero se vuelve engorroso cuando se reutiliza en todo tu código.

Creación de un tipo de diccionario reutilizable

Para una mejor reutilización, define un tipo de diccionario genérico:

// Tipo de diccionario genérico
type Dictionary<K extends string | number | symbol, V> = {
  [key in K]: V;
};

// Uso con claves string y valores numéricos
const scores: Dictionary<string, number> = {};
scores[""math""] = 95;
scores[""science""] = 87;

// Uso con claves numéricas
const idMapping: Dictionary<number, string> = {};
idMapping[1] = ""user_a"";
idMapping[2] = ""user_b"";

Uso del tipo de utilidad Record

El tipo de utilidad Record incorporado en TypeScript proporciona una sintaxis más limpia:

// Uso de Record para un conjunto fijo de claves
type UserFields = ""name"" | ""email"" | ""role"";
const user: Record<UserFields, string> = {
  name: ""Alice Smith"",
  email: ""alice@example.com"",
  role: ""Admin""
};

// Uso de Record con cualquier clave string
const config: Record<string, any> = {};
config[""apiUrl""] = ""https://api.example.com"";
config[""timeout""] = 5000;

El tipo Record es especialmente útil cuando necesitas imponer un conjunto específico de claves utilizando tipos literales de string.

Uso de Map de JavaScript con TypeScript

Para características más avanzadas, usa el objeto Map de JavaScript con genéricos de TypeScript:

// Map con seguridad de tipos
const userProfiles = new Map<string, {age: number, active: boolean}>();

// Añadiendo entradas
userProfiles.set(""alice"", {age: 28, active: true});
userProfiles.set(""bob"", {age: 34, active: false});

// La comprobación de tipos funciona
userProfiles.set(""charlie"", {age: ""thirty""}); // Error: Type 'string' not assignable to type 'number'

// Accediendo a valores
const aliceProfile = userProfiles.get(""alice"");
console.log(aliceProfile?.age); // 28

Comparación de implementaciones de diccionarios

Característica Objeto con firma de índice Tipo Record Map Tipos de clave String, number, symbol Cualquier tipo Cualquier tipo Rendimiento Rápido para conjuntos pequeños Rápido para conjuntos pequeños Mejor para adiciones/eliminaciones frecuentes Uso de memoria Menor Menor Mayor Preservación del orden No No Sí (orden de inserción) Iteración Requiere Object.entries() Requiere Object.entries() Iteradores incorporados Métodos especiales No No has(), delete(), clear() Riesgo de contaminación del prototipo Sí Sí No

Operaciones comunes con diccionarios

Comprobar si existe una clave

// Usando firma 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

Añadir y actualizar valores

// Usando firma de índice
type UserDict = Record<string, {name: string, role: string}>;
const users: UserDict = {};

// Añadiendo
users[""u1""] = {name: ""Alice"", role: ""Admin""};

// Actualizando
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""}); // Actualiza la entrada existente

Eliminar claves

// Usando firma de índice
delete users[""u1""];

// Usando Map
userMap.delete(""u1"");

Iterar a través de diccionarios

// Iteración en diccionario basado en objetos
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]}`);
});

// Iteración en Map
const scoreMap = new Map<string, number>([
  [""math"", 95],
  [""science"", 87]
]);

// Iteración directa
for (const [subject, score] of scoreMap) {
  console.log(`${subject}: ${score}`);
}

// Usando forEach
scoreMap.forEach((score, subject) => {
  console.log(`${subject}: ${score}`);
});

Técnicas avanzadas de seguridad de tipos

Restringir tipos de clave

// Uso de uniones de tipos literales de string
type ValidKeys = ""id"" | ""name"" | ""email"";
const userData: Record<ValidKeys, string> = {
  id: ""123"",
  name: ""Alice"",
  email: ""alice@example.com""
};

// Intento de usar claves no válidas
userData.phone = ""555-1234""; // Error: Property 'phone' does not exist

Diccionarios de solo lectura

// Diccionario inmutable
const constants: Readonly<Record<string, number>> = {
  MAX_USERS: 100,
  TIMEOUT_MS: 5000
};

constants.MAX_USERS = 200; // Error: Cannot assign to 'MAX_USERS' because it is a read-only property

Diccionarios parciales

// Diccionario con valores opcionales
type UserProfile = {
  name: string;
  email: string;
  age: number;
};

// Crear un diccionario donde cada valor puede tener algunas o todas las propiedades de UserProfile
const profiles: Record<string, Partial<UserProfile>> = {};

profiles[""alice""] = { name: ""Alice"" }; // Válido incluso sin email y age
profiles[""bob""] = { name: ""Bob"", email: ""bob@example.com"" }; // Válido

Uso de tipos mapeados para diccionarios avanzados

type SensorData = {
  temperature: number;
  humidity: number;
  pressure: string;
  metadata: object;
};

// Crear un tipo de diccionario que solo incluya propiedades numéricas de SensorData
type NumericSensorDict = {
  [K in keyof SensorData as SensorData[K] extends number ? K : never]: boolean;
};

// El tipo resultante solo tiene temperature y humidity como claves válidas
const sensorStatus: NumericSensorDict = {
  temperature: true,
  humidity: false
};

Consideraciones de rendimiento

Al elegir una implementación de diccionario, considera:

  1. Tamaño de datos: Para conjuntos pequeños con claves string, los diccionarios basados en objetos son eficientes
  2. Modificaciones frecuentes: Map tiene mejor rendimiento para adiciones y eliminaciones frecuentes
  3. Uso de memoria: Los diccionarios basados en objetos usan menos memoria que Map
  4. Tipos de clave: Si necesitas claves no string, usa Map

Resultados de benchmark para 10,000 operaciones:

Acceso a objeto: ~25ms
Acceso a Map: ~45ms
Inserción en objeto: ~30ms
Inserción en Map: ~50ms
Eliminación en objeto: ~20ms
Eliminación en Map: ~15ms

Casos de uso del mundo real

Gestión de configuración

type AppConfig = Record<string, string | number | boolean>;

const config: AppConfig = {
  apiUrl: ""https://api.example.com"",
  timeout: 5000,
  enableCache: true
};

Implementación de caché

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

Gestión de estado en React

interface UserState {
  users: Record<string, {name: string, role: string}>;
  loading: boolean;
  error: string | null;
}

const initialState: UserState = {
  users: {},
  loading: false,
  error: null
};

// En un reductor
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
          }
        }
      };
    // Otros casos...
    default:
      return state;
  }
}

Estrategias de manejo de errores

Manejo de claves inexistentes

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 diccionario

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 es Record<string, {name: string, age: number}>
    for (const [id, user] of Object.entries(data)) {
      console.log(`User ${id}: ${user.name}, ${user.age}`);
    }
  }
}

Conclusión

Los diccionarios en TypeScript proporcionan formas poderosas de gestionar datos de pares clave-valor con seguridad de tipos completa. Al elegir el enfoque de implementación adecuado según tus requisitos específicos, puedes construir aplicaciones más robustas con menos errores en tiempo de ejecución. Ya sea que necesites objetos simples con claves string o diccionarios complejos con características avanzadas, el sistema de tipos de TypeScript garantiza que tus operaciones de diccionario permanezcan seguras en cuanto a tipos en todo tu código.

Preguntas frecuentes

Usa Map cuando necesites claves no string, preservación garantizada del orden de las claves, adiciones y eliminaciones frecuentes, o protección contra la contaminación del prototipo. Los Maps proporcionan métodos incorporados como has(), delete() y clear() que hacen ciertas operaciones más convenientes

Usa tipos literales de string con el tipo de utilidad 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`nnEsto asegura que TypeScript mostrará un error si intentas usar claves que no estén en la unión AllowedKeys

No con diccionarios basados en objetos, pero sí puedes con Map:nn`typescriptnconst userMap = new Map<{id: number}, string>();nconst key1 = {id: 1};nconst key2 = {id: 1}; // Referencia de objeto diferentennuserMap.set(key1, 'Alice');nconsole.log(userMap.get(key1)); // 'Alice'nconsole.log(userMap.get(key2)); // undefined (referencia de objeto diferente)n`nnTen en cuenta que Map usa igualdad de referencia para las claves de objeto

Convertir objeto a Map:nn`typescriptnconst obj: Record<string, number> = {a: 1, b: 2};nconst map = new Map(Object.entries(obj));n`nnConvertir Map a objeto:nn`typescriptnconst mapInstance = new Map<string, number>([['a', 1], ['b', 2]]);nconst objFromMap = Object.fromEntries(mapInstance);n

Usa encadenamiento opcional y coalescencia nula:nn`typescriptnconst users: Record<string, {name: string} | undefined> = {n '1': {name: 'Alice'}n};nn// Acceso seguro con valor alternativonconst userName = users['2']?.name ?? 'Unknown';nconsole.log(userName); // 'Unknown'n`nnEsto previene errores en tiempo de ejecución al acceder a propiedades de valores potencialmente indefinidos

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers