Saltar al contenido
arrays-y-tuplas.ts · devschool

Arrays y tuplas en TypeScript

Lección 4 de 15 · 13 min de lectura · Actualizado el

En esta lección
  1. Tipar un array
  2. Arrays con varios tipos
  3. Arrays de objetos
  4. Los métodos de arrays ya están tipados
  5. Arrays de solo lectura
  6. Arrays de arrays
  7. Tuplas
  8. Errores frecuentes
  9. Resumen

Casi todos los programas trabajan con listas: los productos de un carrito, las notas de un examen, las rutas de un club de montaña. En esta lección aprenderás a tipar arrays en TypeScript, a protegerlos para que nadie los modifique y a usar tuplas, un tipo de array con un número fijo de posiciones en el que cada una tiene su propio tipo. Si necesitas repasar cómo funcionan los arrays en sí, tienes la lección de Arrays de JavaScript.

Tipar un array

Un array de textos se escribe string[]: el tipo de los elementos seguido de corchetes vacíos.

const asignaturas: string[] = ["Programación", "Bases de datos", "Lenguajes de marcas"];
const notas: number[] = [7.5, 6, 9];

asignaturas.push("Sistemas informáticos");
console.log(asignaturas.length, notas[2]);
// 4 9

A partir de aquí TypeScript no te deja meter un elemento de otro tipo:

const notas: number[] = [7.5, 6, 9];
notas.push("10");
// Error: Argument of type 'string' is not assignable to parameter of type 'number'.

Igual que con las variables sueltas, normalmente no hace falta anotar: TypeScript infiere el tipo a partir de los elementos iniciales.

const pesos = [20, 22.5, 25];   // number[]
const dias = ["lunes", "jueves"]; // string[]

pesos.push(27.5);
dias.push(3);
// Error: Argument of type 'number' is not assignable to parameter of type 'string'.

La otra forma: Array<T>

Existe una segunda sintaxis equivalente: Array<string> es exactamente lo mismo que string[]. Es la forma genérica (verás qué significa en Genéricos).

const socios: Array<string> = ["Lucía", "Andrés"];
const cuotas: Array<number> = [35, 40];
console.log(socios[0], cuotas[1]);
// Lucía 40
FormaEjemploCuándo se usa
Corchetesstring[]La más habitual; úsala por defecto
GenéricaArray<string>Con tipos largos o en código que ya usa genéricos

Elige una y sé coherente en todo el proyecto. En este curso usaremos string[].

El array vacío

Cuando un array empieza vacío y lo rellenas después, anota el tipo. Así TypeScript sabe desde el principio qué puede ir dentro:

const pendientes: string[] = [];

pendientes.push("Revisar arnés");
pendientes.push("Comprar magnesio");
console.log(pendientes);
// [ 'Revisar arnés', 'Comprar magnesio' ]

Sin la anotación, TypeScript va deduciendo el tipo según lo que metes, pero el código es menos claro y el error, si lo hay, aparece lejos de donde está el fallo. Anota siempre los arrays vacíos.

Arrays con varios tipos

Si un array mezcla textos y números, TypeScript infiere una unión: (string | number)[], “un array donde cada elemento es un texto o un número”.

const mezcla = ["A-12", 40, "B-07", 15]; // (string | number)[]
mezcla.push(true);
// Error: Argument of type 'boolean' is not assignable to parameter of type 'string | number'.

Los paréntesis importan mucho:

TipoSignifica
(string | number)[]Un array donde cada elemento puede ser texto o número
string[] | number[]O bien un array solo de textos, o bien uno solo de números
string | number[]Un texto suelto, o bien un array de números

Las uniones tienen su propia lección: Uniones y alias. Aun así, mezclar tipos en un mismo array suele ser señal de que en realidad quieres un array de objetos o una tupla, que verás enseguida.

Arrays de objetos

Lo más común en una aplicación real es un array de objetos. Se escribe la forma del objeto y se añaden los corchetes detrás:

const carrito: { producto: string; precio: number; unidades: number }[] = [
  { producto: "Cuaderno", precio: 2.5, unidades: 3 },
  { producto: "Bolígrafo", precio: 1.2, unidades: 5 },
];

let total = 0;
for (const linea of carrito) {
  total += linea.precio * linea.unidades;
}
console.log(total.toFixed(2));
// 13.50

Escribir la forma del objeto en línea es incómodo y no se puede reutilizar. Lo normal es darle un nombre con interface o type y escribir LineaCarrito[]. Lo aprenderás en Objetos e interfaces; en el resto de esta lección ya lo usamos de forma sencilla.

Los métodos de arrays ya están tipados

Todos los métodos que conoces de Métodos de arrays (map, filter, find, reduce…) vienen con sus tipos. No tienes que anotar nada: TypeScript deduce el tipo de los parámetros de las funciones flecha y el del resultado.

interface Ruta {
  nombre: string;
  km: number;
}

const rutas: Ruta[] = [
  { nombre: "Ruta del Cares", km: 12 },
  { nombre: "Senda del Oso", km: 22 },
  { nombre: "Faro de Ajo", km: 6 },
];

const nombres = rutas.map((r) => r.nombre);          // string[]
const largas = rutas.filter((r) => r.km > 10);       // Ruta[]
const kmTotales = rutas.reduce((suma, r) => suma + r.km, 0); // number

console.log(nombres);
console.log(largas.length, kmTotales);
// [ 'Ruta del Cares', 'Senda del Oso', 'Faro de Ajo' ]
// 2 40

Dentro de map, el parámetro r ya es de tipo Ruta sin escribirlo. Si te equivocas en el nombre de una propiedad (r.kms), el editor te avisa al momento.

find puede no encontrar nada

find devuelve el elemento encontrado o undefined, y TypeScript te obliga a tenerlo en cuenta:

interface Ruta {
  nombre: string;
  km: number;
}

const rutas: Ruta[] = [{ nombre: "Faro de Ajo", km: 6 }];

const buscada = rutas.find((r) => r.nombre === "Picu Urriellu"); // Ruta | undefined
console.log(buscada.km);
// Error: 'buscada' is possibly 'undefined'.

Este error te salva de un clásico Cannot read properties of undefined. Hay dos formas habituales de resolverlo: comprobar con if o usar el encadenamiento opcional ?..

interface Ruta {
  nombre: string;
  km: number;
}

const rutas: Ruta[] = [{ nombre: "Faro de Ajo", km: 6 }];
const buscada = rutas.find((r) => r.nombre === "Picu Urriellu");

if (buscada) {
  console.log(`${buscada.nombre}: ${buscada.km} km`);
} else {
  console.log("Ruta no encontrada");
}
console.log(buscada?.km ?? 0);
// Ruta no encontrada
// 0

Cuidado: acceder por índice no está tan protegido. rutas[5] tiene tipo Ruta aunque el array solo tenga un elemento, y al ejecutar obtendrás undefined. La opción noUncheckedIndexedAccess del tsconfig.json hace que el índice devuelva Ruta | undefined; la verás en Módulos y tsconfig.

Arrays de solo lectura

A veces un array no debería cambiar nunca: los días de la semana, las categorías de una tienda o una lista que recibe una función y que no debe tocar. Para eso existe readonly delante del tipo:

const categorias: readonly string[] = ["Escalada", "Senderismo", "Camping"];

console.log(categorias.includes("Camping"));
categorias.push("Esquí");
// Error: Property 'push' does not exist on type 'readonly string[]'.

Un array readonly conserva los métodos que leen (map, filter, includes, slice…) y pierde los que modifican (push, pop, splice, sort, reverse…). Tampoco permite asignar por índice (categorias[0] = "..."). La forma genérica equivalente es ReadonlyArray<string>.

Su uso más práctico es en los parámetros de funciones. Así dejas claro, y TypeScript lo garantiza, que la función no va a modificar el array que le pasan:

function media(valores: readonly number[]): number {
  let suma = 0;
  for (const v of valores) suma += v;
  return valores.length === 0 ? 0 : suma / valores.length;
}

const notasTrimestre = [6.5, 8, 7];
console.log(media(notasTrimestre));
// 7.166666666666667

Fíjate en que puedes pasar un number[] normal a un parámetro readonly number[]. Al revés no se puede: un array de solo lectura no se acepta donde se espera uno modificable.

Consejo: si una función no necesita modificar un array, declara el parámetro como readonly. Es una documentación que el compilador comprueba.

as const con arrays

Ya viste as const en Tipos básicos. Con un array hace dos cosas: lo vuelve de solo lectura y guarda los valores exactos como tipos literales.

const tallas = ["S", "M", "L", "XL"] as const;
// tipo: readonly ["S", "M", "L", "XL"]

type Talla = (typeof tallas)[number]; // "S" | "M" | "L" | "XL"

const elegida: Talla = "M";
console.log(elegida, tallas.length);
// M 4

(typeof tallas)[number] significa “el tipo de cualquier elemento de tallas”. Es un truco muy usado para tener a la vez una lista de valores que puedes recorrer y un tipo con esos mismos valores. Lo verás con más detalle en Enums y literales.

Arrays de arrays

Un array cuyos elementos son arrays (una matriz) se escribe con dos pares de corchetes:

const tablero: string[][] = [
  ["X", "O", "X"],
  ["O", "X", " "],
  [" ", " ", "O"],
];

tablero[1][2] = "X";
console.log(tablero[1].join("|"));
// O|X|X

string[][] se lee de derecha a izquierda: “un array de arrays de textos”.

Tuplas

Una tupla es un array con un número fijo de elementos en el que cada posición tiene su propio tipo. Sirve para agrupar unos pocos datos relacionados sin tener que crear un objeto:

const cima: [string, number] = ["Torre Cerredo", 2648];

console.log(`${cima[0]} mide ${cima[1]} m`);
// Torre Cerredo mide 2648 m

TypeScript comprueba el orden, los tipos y la cantidad de elementos:

const cimaMal: [string, number] = [2648, "Torre Cerredo"];
// Error: Type 'number' is not assignable to type 'string'.
// Error: Type 'string' is not assignable to type 'number'.

const cima: [string, number] = ["Torre Cerredo", 2648];
console.log(cima[2]);
// Error: Tuple type '[string, number]' of length '2' has no element at index '2'.

Nota: las tuplas solo existen en TypeScript. Al compilar, una tupla es un array normal de JavaScript.

Las tuplas no se infieren solas

Si escribes const cima = ["Torre Cerredo", 2648] sin anotar, TypeScript infiere (string | number)[], un array de cualquier longitud. Para tener una tupla tienes que anotarla o usar as const (que además la hace de solo lectura):

const sinAnotar = ["Torre Cerredo", 2648];          // (string | number)[]
const anotada: [string, number] = ["Peña Vieja", 2613]; // [string, number]
const fija = ["Pico Tesorero", 2570] as const;      // readonly ["Pico Tesorero", 2570]

console.log(sinAnotar.length, anotada[0], fija[1]);
// 2 Peña Vieja 2570

Desestructurar tuplas

Las tuplas se llevan muy bien con la desestructuración: cada variable recibe el tipo de su posición.

const marca: [string, number, boolean] = ["Elena", 42.3, true];
const [corredora, segundos, esRecord] = marca;
// corredora: string, segundos: number, esRecord: boolean

console.log(`${corredora}: ${segundos} s${esRecord ? " (récord)" : ""}`);
// Elena: 42.3 s (récord)

Devolver varios valores desde una función

El uso más habitual de las tuplas es que una función devuelva dos o tres valores sin crear un objeto. Si has visto React, useState funciona así.

function dividirCuenta(total: number, personas: number): [number, number] {
  const porPersona = Math.floor((total / personas) * 100) / 100;
  const sobra = Math.round((total - porPersona * personas) * 100) / 100;
  return [porPersona, sobra];
}

const [cadaUno, resto] = dividirCuenta(100, 3);
console.log(cadaUno, resto);
// 33.33 0.01

Tuplas con etiquetas

Puedes poner un nombre a cada posición. No cambia nada al ejecutar, pero el editor muestra esos nombres y el código se entiende mejor:

type Coordenada = [latitud: number, longitud: number];

const santander: Coordenada = [43.4623, -3.81];
const [lat, lon] = santander;
console.log(`Lat ${lat}, Lon ${lon}`);
// Lat 43.4623, Lon -3.81

La palabra type crea un alias, un nombre para un tipo. Lo verás a fondo en Uniones y alias.

Elementos opcionales y resto

Una posición puede ser opcional con ? (siempre al final), y con ... indicas que a partir de cierta posición puede haber cualquier cantidad de elementos de un tipo:

type Contacto = [nombre: string, telefono: string, email?: string];

const c1: Contacto = ["Mario", "600111222"];
const c2: Contacto = ["Sara", "600333444", "sara@correo.es"];

type Clasificacion = [prueba: string, ...tiempos: number[]];

const sprint: Clasificacion = ["100 m lisos", 11.2, 11.5, 12.1];
const [prueba, ...tiempos] = sprint;

console.log(c1.length, c2[2]);
console.log(prueba, Math.min(...tiempos));
// 2 sara@correo.es
// 100 m lisos 11.2

Tuplas de solo lectura

Una trampa de las tuplas: como en el fondo son arrays, push sí está permitido, y entonces la longitud real ya no coincide con el tipo.

const punto: [number, number] = [3, 4];
punto.push(5);              // TypeScript no protesta
console.log(punto);
// [ 3, 4, 5 ]

Para evitarlo, marca la tupla como readonly. Es buena idea siempre que no necesites modificarla:

const origen: readonly [number, number] = [0, 0];
origen.push(1);
// Error: Property 'push' does not exist on type 'readonly [number, number]'.

¿Tupla u objeto?

Usa una tupla cuando…Usa un objeto cuando…
Son 2 o 3 valores y el orden es evidente (coordenadas, par clave-valor)Hay más de 3 datos
Una función devuelve varios valores que se desestructuran al momentoLos datos viajan por el programa o se guardan
Quieres nombrar las variables al desestructurarQuieres que cada dato tenga un nombre fijo

["Elena", 42.3, true] se entiende en la línea en que lo creas, pero dentro de diez archivos nadie sabrá qué era el true. Con más de tres valores, un objeto siempre es más claro.

Errores frecuentes

  • Declarar un array vacío sin tipo: const lista = [] obliga a TypeScript a adivinar. Escribe const lista: string[] = [].
  • Confundir (string | number)[] con string[] | number[]: el primero mezcla tipos en el mismo array; el segundo es un array de un tipo o de otro.
  • Olvidar que find puede devolver undefined: comprueba el resultado con if o usa ?. y ??.
  • Fiarse del acceso por índice: lista[10] tiene el tipo del elemento aunque no exista. Comprueba la longitud o activa noUncheckedIndexedAccess.
  • Esperar que TypeScript infiera una tupla: ["a", 1] se infiere como (string | number)[]. Anótala o usa as const.
  • Usar push en una tupla: está permitido y rompe la longitud. Declárala readonly.
  • Tuplas con demasiadas posiciones: a partir de tres valores, un objeto con nombres es más legible.

Resumen

ConceptoEjemplo
Arraystring[] o Array<string>
Array vacíoconst lista: number[] = []
Array con varios tipos(string | number)[]
Array de objetosRuta[]
findDevuelve T | undefined
Solo lecturareadonly string[] o ReadonlyArray<string>
as const["S", "M"] as const → readonly ["S", "M"]
Matrizstring[][]
Tupla[string, number]
Con etiquetas[latitud: number, longitud: number]
Opcional y resto[string, number?], [string, ...number[]]
Tupla de solo lecturareadonly [number, number]

En la siguiente lección aprenderás a dar nombre a la forma de tus objetos: Objetos e interfaces.

Pon a prueba lo que has aprendido

[TypeScript] ¿Qué tipo infiere TypeScript para pesos?
const pesos = ["A-12", 40, "B-07", 15];

[TypeScript] ¿Qué ocurre con la última línea?
const cima: [string, number] = ["Torre Cerredo", 2648];
console.log(cima[2]);

[TypeScript] ¿Qué hace TypeScript con esta línea?
const punto: [number, number] = [3, 4];
punto.push(5);

[TypeScript] ¿Qué error da esta llamada?
const categorias: readonly string[] = ["Escalada", "Camping"];
categorias.push("Esquí");

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.