En esta página

Arrays, tuplas y enums

14 min lectura TextoCap. 2 — Tipos compuestos

Arrays, tuplas y enums

Los arreglos, tuplas y enumeraciones son las estructuras de datos fundamentales de TypeScript. Cada una resuelve un problema distinto: los arreglos almacenan colecciones de elementos del mismo tipo, las tuplas modelan secuencias de longitud fija con tipos heterogéneos, y los enums nombran conjuntos de constantes relacionadas. Entender cuándo usar cada una hace la diferencia entre código expresivo y código confuso.

Arrays tipados

TypeScript ofrece dos sintaxis equivalentes para arreglos tipados:

// Sintaxis 1: tipo seguido de []
const nombres: string[] = ['Ana', 'Carlos', 'María'];
const precios: number[] = [9.99, 14.99, 4.99];
const activos: boolean[] = [true, false, true];

// Sintaxis 2: genérico Array<T>
const nombres2: Array<string> = ['Ana', 'Carlos', 'María'];
const precios2: Array<number> = [9.99, 14.99, 4.99];

Ambas son idénticas. La primera es más concisa y la más común en la práctica. La segunda es preferible cuando el tipo genérico es complejo (por legibilidad).

Arreglos de objetos

interface Producto {
  id: string;
  nombre: string;
  precio: number;
  enStock: boolean;
}

const catalogo: Producto[] = [
  { id: 'p1', nombre: 'Laptop', precio: 1299.99, enStock: true },
  { id: 'p2', nombre: 'Mouse', precio: 29.99, enStock: false },
];

// TypeScript conoce cada propiedad:
const primerProducto = catalogo[0]; // tipo: Producto | undefined (con noUncheckedIndexedAccess)

Métodos de arreglos tipados

TypeScript infiere correctamente los tipos de retorno de los métodos de arreglos:

const numeros: number[] = [1, 2, 3, 4, 5];

const dobles = numeros.map(n => n * 2);         // number[]
const pares = numeros.filter(n => n % 2 === 0); // number[]
const suma = numeros.reduce((acc, n) => acc + n, 0); // number
const primero = numeros.find(n => n > 3);       // number | undefined
const existe = numeros.some(n => n > 4);        // boolean
const índice = numeros.findIndex(n => n === 3); // number

Arreglos de solo lectura

Los arreglos readonly son fundamentales para diseñar APIs seguras. Impiden mutaciones accidentales.

// readonly T[] y ReadonlyArray<T> son equivalentes
function sumarTodos(numeros: readonly number[]): number {
  // numeros.push(4);   // ❌ Error: Property 'push' does not exist on type 'readonly number[]'
  // numeros[0] = 99;   // ❌ Error: Index signature in type 'readonly number[]' only permits reading
  return numeros.reduce((acc, n) => acc + n, 0); // ✅
}

const lista = [1, 2, 3];
sumarTodos(lista); // ✅ Un number[] normal es asignable a readonly number[]

La regla de asignabilidad: puedes pasar un arreglo mutable donde se espera uno readonly, pero no al revés.

const mutable: number[] = [1, 2, 3];
const inmutable: readonly number[] = mutable; // ✅

const origen: readonly number[] = [1, 2, 3];
const destino: number[] = origen; // ❌ Error

Arreglos anidados (matrices)

type Matriz = number[][];
const tablero: Matriz = [
  [1, 0, 0],
  [0, 1, 0],
  [0, 0, 1],
];

// O con genérico:
type MatrizGenérica<T> = T[][];
const celdas: MatrizGenérica<string> = [['a', 'b'], ['c', 'd']];

Tuplas: arreglos de longitud y tipos fijos

Una tupla es un arreglo donde TypeScript conoce exactamente cuántos elementos hay y qué tipo tiene cada posición. Son ideales para representar datos estructurados donde el orden importa.

// Tupla básica: [string, number]
type Coordenada = [number, number];
const punto: Coordenada = [40.7128, -74.0060]; // [latitud, longitud]
// punto[0] es number, punto[1] es number

// Tupla heterogénea
type EntradaRegistro = [string, number, boolean];
const entrada: EntradaRegistro = ['2025-04-02', 42, true];
const [fecha, valor, procesado] = entrada; // Desestructuración tipada

Tuplas nombradas: mucho más legibles

TypeScript permite nombrar cada elemento de la tupla, lo que hace el código considerablemente más expresivo:

// Sin nombres: ¿qué significa cada posición?
type ResultadoBúsqueda = [string, number, boolean];

// Con nombres: auto-documenta el código
type ResultadoBúsquedaNombrado = [id: string, relevancia: number, exacto: boolean];

const resultado: ResultadoBúsquedaNombrado = ['usr-123', 0.95, true];
const [id, relevancia, exacto] = resultado;

Los nombres en tuplas también mejoran el autocompletado: cuando desestructuras, el editor muestra los nombres en lugar de índices genéricos.

Tuplas con elementos opcionales y rest

// Elemento opcional al final
type ConFecha = [valor: number, unidad: string, fecha?: Date];
const medición1: ConFecha = [98.6, 'F'];
const medición2: ConFecha = [37, 'C', new Date()];

// Rest elements: una o más al final
type MínimoUno = [primero: string, ...resto: string[]];
const ruta: MínimoUno = ['inicio'];
const ruta2: MínimoUno = ['inicio', 'paso1', 'paso2', 'fin'];

Tuplas de solo lectura

const punto: readonly [number, number] = [3, 4];
// punto[0] = 5; // ❌ Error: Cannot assign to '0' because it is a read-only property

Caso de uso real: valores de retorno múltiples

Las tuplas son el patrón idiomático para funciones que retornan múltiples valores (similar a los hooks de React):

function usarContador(inicial: number): [valor: number, incrementar: () => void, reiniciar: () => void] {
  let valor = inicial;
  const incrementar = (): void => { valor++; };
  const reiniciar = (): void => { valor = inicial; };
  return [valor, incrementar, reiniciar];
}

const [cuenta, inc, reset] = usarContador(0);

Enumeraciones (enums)

Los enums permiten definir un conjunto de constantes nombradas. TypeScript ofrece varias variantes con características y trade-offs distintos.

Enums numéricos

enum Dirección {
  Arriba,    // 0
  Abajo,     // 1
  Izquierda, // 2
  Derecha,   // 3
}

const movimiento: Dirección = Dirección.Arriba;
console.log(movimiento); // 0
console.log(Dirección[0]); // 'Arriba' (mapeo inverso)

Los enums numéricos compilan a un objeto JavaScript bidireccional (nombre → valor y valor → nombre). Esto permite el mapeo inverso pero genera más código.

Puedes establecer valores numéricos explícitos:

enum CódigoHTTP {
  OK = 200,
  Creado = 201,
  NoEncontrado = 404,
  ErrorServidor = 500,
}

Enums de string: más seguros y legibles

enum EstadoTarea {
  Pendiente = 'pendiente',
  EnProgreso = 'en_progreso',
  Completada = 'completada',
  Cancelada = 'cancelada',
}

function procesarTarea(estado: EstadoTarea): string {
  switch (estado) {
    case EstadoTarea.Pendiente: return 'La tarea está en cola';
    case EstadoTarea.EnProgreso: return 'La tarea se está ejecutando';
    case EstadoTarea.Completada: return 'La tarea finalizó exitosamente';
    case EstadoTarea.Cancelada: return 'La tarea fue cancelada';
  }
}

const estado: EstadoTarea = EstadoTarea.Completada;
console.log(estado); // 'completada' (el string, no un número)

Los enums de string son preferibles a los numéricos en casi todos los casos: el valor es legible en logs y en el debugger, no tienen mapeo inverso (más simple), y los valores son estables si se reordenan.

const enum: inlining en tiempo de compilación

La variante const enum instruye al compilador a sustituir cada uso del enum con su valor literal durante la compilación:

const enum Prioridad {
  Baja = 1,
  Media = 2,
  Alta = 3,
  Crítica = 4,
}

const tarea = { prioridad: Prioridad.Alta };
// Compila a:
// const tarea = { prioridad: 3 };
// El objeto enum no existe en el JavaScript generado

El beneficio es rendimiento: cero código generado para el enum, cero accesos de propiedad en runtime. El costo es que no puedes iterar los valores del enum ni usar el mapeo inverso, porque el objeto no existe.

Enums vs union types de literales

Para muchos casos de uso, una union type de literales es más simple y flexible que un enum:

// Enum
enum RolUsuario {
  Admin = 'admin',
  Editor = 'editor',
  Lector = 'lector',
}

// Union type literal (alternativa más ligera)
type RolUsuario = 'admin' | 'editor' | 'lector';

Usa enum cuando:

  • Necesitas iterar los valores (Object.values(MiEnum)).
  • Quieres agrupar constantes relacionadas bajo un namespace.
  • Estás en un contexto donde los valores pueden cambiar y quieres un punto central de actualización.

Usa union type cuando:

  • El conjunto de valores es pequeño y estable.
  • Quieres un tipo más liviano sin generar código JavaScript adicional.
  • Necesitas compatibilidad con bibliotecas que usan strings directamente.

Ejemplo completo: sistema de pedidos

Combinemos todo lo aprendido en un modelo de dominio real:

const enum EstadoPedido {
  Borrador = 'borrador',
  Confirmado = 'confirmado',
  Preparando = 'preparando',
  EnCamino = 'en_camino',
  Entregado = 'entregado',
  Cancelado = 'cancelado',
}

interface LineaPedido {
  readonly productoId: string;
  readonly nombre: string;
  readonly precioUnitario: number;
  cantidad: number;
}

type Pedido = {
  readonly id: string;
  readonly clienteId: string;
  readonly líneas: readonly LineaPedido[];
  estado: EstadoPedido;
  readonly creadoEn: Date;
  readonly total: number;
};

function crearPedido(
  id: string,
  clienteId: string,
  líneas: readonly LineaPedido[]
): Pedido {
  const total = líneas.reduce(
    (acc, línea) => acc + línea.precioUnitario * línea.cantidad,
    0
  );
  return {
    id,
    clienteId,
    líneas,
    estado: EstadoPedido.Borrador,
    creadoEn: new Date(),
    total,
  };
}

function confirmarPedido(pedido: Pedido): Pedido {
  if (pedido.estado !== EstadoPedido.Borrador) {
    throw new Error(`No se puede confirmar un pedido en estado: ${pedido.estado}`);
  }
  return { ...pedido, estado: EstadoPedido.Confirmado };
}

// Uso
const líneas: LineaPedido[] = [
  { productoId: 'p1', nombre: 'Audífonos', precioUnitario: 59.99, cantidad: 2 },
  { productoId: 'p2', nombre: 'Cable USB-C', precioUnitario: 9.99, cantidad: 3 },
];

const pedido = crearPedido('ord-001', 'usr-123', líneas);
console.log(pedido.total);  // 149.95
const pedidoConfirmado = confirmarPedido(pedido);
console.log(pedidoConfirmado.estado); // 'confirmado'

En la siguiente lección daremos un salto importante al aprender a modelar datos con interfaces y type aliases, las dos herramientas más usadas en TypeScript para describir la forma de los objetos.

Prefiere const enum para rendimiento
Los 'const enum' se inlinan en tiempo de compilación: TypeScript reemplaza cada referencia al enum con su valor literal. El resultado es JavaScript sin ningún objeto enum, lo que reduce el bundle y elimina llamadas de propiedad en tiempo de ejecución.
const enum y bundlers externos
Si usas esbuild, SWC o Babel para transpilar (en lugar de tsc directamente), los 'const enum' pueden causar errores porque esas herramientas procesan archivos de forma aislada. En ese caso, usa enums regulares o union types literales ('activo' | 'inactivo').
readonly en arrays: inmutabilidad controlada
ReadonlyArray<T> y readonly T[] son idénticos. Impiden push(), pop(), splice() y asignación por índice. Son ideales para props de componentes y parámetros de funciones puras. No confundir con Object.freeze(), que es una verificación en runtime.