tutoriales.com

Tipado de Colecciones y Estructuras de Datos Avanzadas en TypeScript: Maps, Sets y Tuplas

Este tutorial profundiza en el tipado de estructuras de datos avanzadas como Maps, Sets y Tuplas en TypeScript. Aprenderás a declarar y manipular estas colecciones con seguridad de tipos, mejorando la robustez y legibilidad de tu código.

Intermedio18 min de lectura18 views
Reportar error

🚀 Introducción al Tipado de Colecciones Avanzadas

TypeScript nos permite escribir código más robusto y predecible al agregar un sistema de tipos estático a JavaScript. Si bien es común tipar arrays y objetos, estructuras de datos como Map, Set y las Tuplas (cuando no son simples arrays) a menudo se pasan por alto o se tipan incorrectamente. Dominar su tipado es crucial para desarrollar aplicaciones complejas y mantener la seguridad de tipos en todo tu proyecto.

En este tutorial, exploraremos en detalle cómo tipar estas colecciones avanzadas, proporcionando ejemplos prácticos y las mejores prácticas para integrarlas en tus proyectos TypeScript. ¡Prepárate para llevar tu conocimiento de tipos a un nuevo nivel! ✨

📌 ¿Por qué tipar Maps, Sets y Tuplas?

La principal razón para tipar estas estructuras de datos es la seguridad y mantenibilidad del código. Sin un tipado adecuado, el compilador de TypeScript no puede detectar errores relacionados con el tipo de los elementos almacenados o la estructura de las tuplas, lo que puede llevar a errores en tiempo de ejecución.

💡 Consejo: Un buen tipado reduce la necesidad de comentarios excesivos y mejora la legibilidad del código, actuando como documentación viva.

🗺️ Tipado de la Clase Map en TypeScript

La clase Map de JavaScript es una colección de pares clave-valor donde las claves pueden ser de cualquier tipo (no solo cadenas como en los objetos). TypeScript ofrece una forma directa de tipar Maps utilizando genéricos.

Declaración Básica de Map

Para tipar un Map, especificas el tipo de la clave y el tipo del valor entre corchetes angulares <KeyType, ValueType>.

// Map donde las claves son números y los valores son cadenas
const usuariosById: Map<number, string> = new Map();

usuariosById.set(1, "Alice");
usuariosById.set(2, "Bob");
// usuariosById.set("3", "Charlie"); // Error: Argumento de tipo 'string' no asignable a parámetro de tipo 'number'.
usuariosById.set(3, "Charlie");

console.log(usuariosById.get(1)); // "Alice"

Usando Tipos de Objeto como Claves o Valores

Map es muy útil cuando necesitas usar objetos como claves.

interface Usuario {
  id: number;
  nombre: string;
}

interface Perfil {
  edad: number;
  ciudad: string;
}

// Map donde las claves son objetos Usuario y los valores son objetos Perfil
const perfilesDeUsuario: Map<Usuario, Perfil> = new Map();

const usuario1: Usuario = { id: 1, nombre: "David" };
const usuario2: Usuario = { id: 2, nombre: "Eva" };

perfilesDeUsuario.set(usuario1, { edad: 30, ciudad: "Madrid" });
perfilesDeUsuario.set(usuario2, { edad: 25, ciudad: "Barcelona" });

console.log(perfilesDeUsuario.get(usuario1)); // { edad: 30, ciudad: 'Madrid' }
📌 Nota: Los objetos se comparan por referencia en las claves de un `Map`. Dos objetos idénticos en valor pero diferentes en referencia serán tratados como claves distintas.

Métodos de Map con Tipado

TypeScript tipa automáticamente los métodos de Map (como set, get, has, delete, keys, values, entries) basándose en los tipos que declaraste.

const productosStock: Map<string, number> = new Map();
productosStock.set("Manzanas", 100);
productosStock.set("Peras", 75);

// Iterando sobre claves y valores
for (const nombre of productosStock.keys()) {
  console.log(`Producto: ${nombre}`); // nombre es de tipo 'string'
}

for (const cantidad of productosStock.values()) {
  console.log(`Cantidad: ${cantidad}`); // cantidad es de tipo 'number'
}

for (const [nombre, cantidad] of productosStock.entries()) {
  console.log(`${nombre}: ${cantidad}`); // nombre es 'string', cantidad es 'number'
}

productosStock.forEach((cantidad, nombre) => {
  console.log(`${nombre} tiene ${cantidad} unidades.`); // 'cantidad' es number, 'nombre' es string
});

Construyendo un Map a partir de un Array

También puedes inicializar un Map a partir de un array de pares [key, value]. Es importante que este array también esté correctamente tipado.

type Coordinador = [string, { telefono: string; email: string }];

const listaCoordinadores: Coordinador[] = [
  ["Ana", { telefono: "123-456-789", email: "ana@example.com" }],
  ["Beto", { telefono: "987-654-321", email: "beto@example.com" }],
];

const coordinadores: Map<string, { telefono: string; email: string }> = new Map(listaCoordinadores);

console.log(coordinadores.get("Ana"));

🌿 Tipado de la Clase Set en TypeScript

La clase Set es una colección de valores únicos de cualquier tipo. TypeScript la tipa de manera similar a Map, usando un solo tipo genérico para los elementos que contiene.

Declaración Básica de Set

Para tipar un Set, especificas el tipo de los elementos que contendrá entre corchetes angulares <ElementType>.

// Set de números únicos
const idsUnicos: Set<number> = new Set();

idsUnicos.add(10);
idsUnicos.add(20);
idsUnicos.add(10); // No se agrega, ya existe
// idsUnicos.add("30"); // Error: Argumento de tipo 'string' no asignable a parámetro de tipo 'number'.
idsUnicos.add(30);

console.log(idsUnicos.size); // 3

Usando Tipos de Objeto en un Set

Al igual que con Map, puedes almacenar objetos en un Set.

interface Tag {
  id: string;
  nombre: string;
}

const tagA: Tag = { id: "ts", nombre: "TypeScript" };
const tagB: Tag = { id: "js", nombre: "JavaScript" };
const tagC: Tag = { id: "ts", nombre: "TypeScript" }; // Mismo valor, diferente referencia

// Set de objetos Tag
const tagsUnicos: Set<Tag> = new Set();

tagsUnicos.add(tagA);
tagsUnicos.add(tagB);
tagsUnicos.add(tagC); // Se agregará porque es una referencia de objeto diferente a tagA

console.log(tagsUnicos.size); // 3 (si tagA y tagC son objetos diferentes en memoria)

// Si quisiéramos que fueran 'únicos' por id, necesitaríamos una lógica personalizada o usar un Map.
⚠️ Advertencia: Para que los objetos sean considerados únicos en un `Set`, deben ser la misma referencia de objeto. Objetos con los mismos valores pero diferentes referencias se considerarán distintos.

Métodos de Set con Tipado

Los métodos de Set como add, has, delete e forEach también se benefician del tipado genérico.

const permisosUsuario: Set<string> = new Set();
permisosUsuario.add("admin");
permisosUsuario.add("editor");

if (permisosUsuario.has("admin")) {
  console.log("El usuario tiene permiso de administrador.");
}

// Iterando sobre los elementos
for (const permiso of permisosUsuario) {
  console.log(`Permiso: ${permiso}`); // permiso es de tipo 'string'
}

permisosUsuario.forEach(permiso => {
  console.log(`Permiso encontrado: ${permiso}`); // 'permiso' es string
});

Inicialización de Set a partir de un Array

const coloresFavoritos: string[] = ["rojo", "azul", "verde", "azul"];
const conjuntoColores: Set<string> = new Set(coloresFavoritos);

console.log(conjuntoColores); // Set { 'rojo', 'azul', 'verde' }

🤝 Tipado de Tuplas en TypeScript

Las tuplas en TypeScript son arrays con un número fijo de elementos donde cada elemento puede tener un tipo diferente y conocido. Son muy útiles para representar registros de datos con una estructura específica.

Declaración Básica de Tuplas

Las tuplas se declaran especificando los tipos de sus elementos en un array literal de tipos [Type1, Type2, ..., TypeN].

// Tupla para representar un punto 2D (x, y)
let punto2D: [number, number];
punto2D = [10, 20];
// punto2D = [10, "20"]; // Error: Argumento de tipo 'string' no asignable a parámetro de tipo 'number'.
// punto2D = [10, 20, 30]; // Error: El tipo '[number, number, number]' no es asignable al tipo '[number, number]'.

console.log(`X: ${punto2D[0]}, Y: ${punto2D[1]}`);

// Tupla para representar información de usuario (id, nombre, activo)
let usuarioInfo: [number, string, boolean];
usuarioInfo = [1, "Alice", true];

Tuplas con Nombres de Elementos (TypeScript 4.0+)

Para mejorar la legibilidad, TypeScript 4.0 introdujo la capacidad de nombrar los elementos de una tupla. Esto no cambia el tipo, pero actúa como documentación.

// Tupla con nombres para los elementos
type Coordenada3D = [x: number, y: number, z: number];

const posicion: Coordenada3D = [1, 2, 3];
console.log(`Posición Z: ${posicion[2]}`);

// Acceso sigue siendo por índice
// console.log(posicion.z); // Error: La propiedad 'z' no existe en el tipo 'Coordenada3D'.
🔥 Importante: Los nombres en las tuplas son solo para fines de legibilidad y no introducen propiedades con nombre en el objeto de la tupla.

Tuplas con Elementos Opcionales y Rest Parameters

TypeScript también soporta elementos opcionales (?) y rest parameters (...) en las tuplas, lo que las hace aún más flexibles.

// Tupla con un elemento opcional
type RespuestaAPI = [status: number, data?: object, error?: string];

const exito: RespuestaAPI = [200, { mensaje: "OK" }];
const fallo: RespuestaAPI = [500, undefined, "Error del servidor"];
const soloStatus: RespuestaAPI = [204];

console.log(exito[1]);

// Tupla con un rest parameter
type LogEntry = [timestamp: number, level: string, ...mensajes: string[]];

const log1: LogEntry = [Date.now(), "INFO", "Aplicación iniciada"];
const log2: LogEntry = [Date.now(), "ERROR", "Archivo no encontrado", "Ruta: /src/app.ts"];

console.log(log2[2]); // "Archivo no encontrado"
console.log(log2[3]); // "Ruta: /src/app.ts"

Desestructuración de Tuplas

La desestructuración de tuplas es una característica muy poderosa, y TypeScript asegura que los tipos se mantengan correctos.

type Persona = [nombre: string, edad: number, esEstudiante: boolean];

const david: Persona = ["David", 30, false];

const [nombre, edad, esEstudiante] = david;

console.log(`${nombre} tiene ${edad} años.`); // nombre es string, edad es number

// const [nombre, edad, esEstudiante, ciudad] = david; // Error: La longitud de la tupla '[string, number, boolean]' en el índice '3' es demasiado corta.

🔄 Casos de Uso Comunes y Mejores Prácticas

Cuando usar Map vs. Objeto Record

CaracterísticaMap<KeyType, ValueType>Record<KeyType, ValueType>
---------
Tipo de ClaveCualquier tipo (objeto, función, primitivo)Generalmente string o number (literales)
Orden de InserciónMantiene el orden de inserciónNo garantiza el orden de inserción
---------
RendimientoMejor rendimiento con muchas entradas o claves no-stringÓptimo para objetos pequeños con claves string estáticas
APIsMétodos específicos (set, get, has, size, forEach, keys, values, entries)Acceso directo por propiedad (obj.key, obj['key']), Object.keys, Object.values
---------
Uso TípicoCaches, colecciones donde las claves son objetos, diccionarios dinámicosConfiguración, DTOs, objetos simples
💡 Consejo: Si necesitas usar objetos como claves o mantener el orden de inserción, `Map` es la elección correcta. Para estructuras clave-valor simples donde las claves son cadenas conocidas, un objeto literal tipado con `Record` o una interfaz puede ser más directo.

Cuando usar Set vs. Array con Unicidad Manual

Set es ideal cuando la unicidad de los elementos es una preocupación principal y la performance en la búsqueda o adición es crítica.

// Con Set
const invitados: Set<string> = new Set();
invitados.add("Ana");
invitados.add("Juan");
invitados.add("Ana"); // Ignorado

console.log(invitados.has("Juan")); // true

// Con Array (requiere lógica manual para unicidad)
const asistentes: string[] = [];
function addAsistente(nombre: string) {
  if (!asistentes.includes(nombre)) {
    asistentes.push(nombre);
  }
}
addAsistente("Maria");
addAsistente("Pedro");
addAsistente("Maria");

console.log(asistentes.includes("Pedro")); // true
📌 Nota: Para verificar la existencia de un elemento en un `Set`, la complejidad es O(1) en promedio, mientras que en un array es O(n).

Uso Efectivo de Tuplas

Las tuplas son excelentes para:

  • Retorno de múltiples valores de una función: Cuando una función necesita devolver varios valores relacionados que tienen diferentes tipos.
function obtenerCoordenadas(): [number, number] {
return [100, 200];
}
const [x, y] = obtenerCoordenadas();
console.log(x, y);
  • Representar pares clave-valor fijos: Aunque Map se usa para colecciones, una tupla puede representar un único par clave-valor fijo.
type ParConfig = [key: string, value: string | number | boolean];
const configuracion: ParConfig = ["theme", "dark"];
  • Registros de base de datos o CSV: Cuando cada fila tiene una estructura predefinida de tipos.
⚠️ Advertencia: Si la estructura de datos es compleja o tiene muchos elementos, considera usar una interfaz o un tipo de objeto en lugar de una tupla para mejorar la legibilidad y la extensibilidad. Las tuplas son mejores para un número pequeño y fijo de elementos.

🛠️ Herramientas y Patrones Avanzados

ReadonlyMap y ReadonlySet

Para colecciones inmutables, TypeScript proporciona los tipos ReadonlyMap y ReadonlySet. Estos tipos eliminan los métodos que mutan la colección (set, add, delete, clear), asegurando que la colección no pueda ser modificada después de su creación.

const precios: ReadonlyMap<string, number> = new Map([["Leche", 1.5], ["Pan", 2.0]]);
// precios.set("Agua", 1.0); // Error: La propiedad 'set' no existe en el tipo 'ReadonlyMap<string, number>'.

const permisosDefecto: ReadonlySet<string> = new Set(["ver", "editar"]);
// permisosDefecto.add("eliminar"); // Error: La propiedad 'add' no existe en el tipo 'ReadonlySet<string>'.
¿Por qué usar `ReadonlyMap` / `ReadonlySet`? Ayudan a enforce la inmutabilidad, lo cual es beneficioso en arquitecturas como Redux o React con estados complejos, donde evitar mutaciones accidentales es crucial. También mejora la inferencia de que una colección no cambiará durante su ciclo de vida.

Transformando Colecciones Tipadas

Es común necesitar transformar una colección en otra. TypeScript te ayuda a mantener los tipos correctos durante estas operaciones.

// De Map a Array de Tuplas
const ciudadesPoblacion: Map<string, number> = new Map([
  ["Tokyo", 37_000_000],
  ["Delhi", 32_000_000]
]);

const arrayCiudades: Array<[string, number]> = Array.from(ciudadesPoblacion.entries());
console.log(arrayCiudades); // [ [ 'Tokyo', 37000000 ], [ 'Delhi', 32000000 ] ]

// De Set a Array
const frutasFavoritas: Set<string> = new Set(["manzana", "banana", "cereza"]);
const arrayFrutas: string[] = Array.from(frutasFavoritas);
console.log(arrayFrutas); // [ 'manzana', 'banana', 'cereza' ]

Diagrama de Flujo: Elegir la Colección Adecuada

¿Necesito almacenar múltiples valores? NO ¿Número fijo y tipos distintos? Tupla ¿Necesito claves? NO ¿Valores únicos? Set NO Array ¿Claves objetos o orden inserción? Map NO ¿Claves estáticas y strings? Objeto

Este diagrama te ayudará a decidir qué estructura de datos utilizar en función de tus necesidades:

Tutorial Completado

✅ Conclusión

Dominar el tipado de Map, Set y Tuplas en TypeScript es un paso crucial para escribir código más seguro, legible y mantenible. Al aplicar los genéricos y las características específicas de las tuplas, puedes asegurarte de que tus colecciones se comporten como esperas y detectar errores de tipo en tiempo de compilación, no en tiempo de ejecución.

Esperamos que esta guía te haya proporcionado las herramientas y el conocimiento necesario para integrar estas poderosas estructuras de datos tipadas en tus proyectos TypeScript. ¡Feliz tipado! 🥳

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!