Tipos de Objetos

Tipos de objetos en TypeScript

En JavaScript, la forma fundamental en que agrupamos y pasamos datos es a través de objetos. En TypeScript, los representamos a través de tipos de objetos.

Como hemos visto, pueden ser anónimos:

Prueba este código ↗

function greet(person: { name: string; age: number }) {
  return "Hello " + person.name;
}

o se les puede nombrar usando una interfaz:

Prueba este código ↗

interface Person {
  name: string;
  age: number;
}
 
function greet(person: Person) {
  return "Hello " + person.name;
}

o un alias de tipo:

Prueba este código ↗

type Person = {
  name: string;
  age: number;
};
 
function greet(person: Person) {
  return "Hello " + person.name;
}

En los tres ejemplos anteriores, hemos escrito funciones que toman objetos que contienen la propiedad name (que debe ser un string) y age (que debe ser un number).

Referencia rápida

Tenemos hojas de trucos disponibles para type e interface, si quieres echarle un vistazo rápido la sintaxis cotidiana.

Modificadores de propiedades

Cada propiedad en un tipo de objeto puede especificar un par de cosas: el tipo, si la propiedad es opcional y si se puede escribir en la propiedad.

Propiedades opcionales

La mayor parte del tiempo, nos encontraremos tratando con objetos que podrían tener una propiedad establecida. En esos casos, podemos marcar esas propiedades como opcionales agregando un signo de interrogación (?) al final de sus nombres.

Prueba este código ↗

interface PaintOptions {
  shape: Shape;
  xPos?: number;
  yPos?: number;
}
 
function paintShape(opts: PaintOptions) {
  // ...
}
 
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });

En este ejemplo, tanto xPos como yPos se consideran opcionales. Podemos optar por proporcionar cualquiera de ellos, por lo que todas las llamadas anteriores a paintShape son válidas. Lo único que realmente dice la opcionalidad es que si la propiedad está configurada, es mejor que tenga un tipo específico.

También podemos leer desde esas propiedades, pero cuando lo hacemos en strictNullChecks, TypeScript nos dirá que son potencialmente undefined.

Prueba este código ↗

function paintShape(opts: PaintOptions) {
  let xPos = opts.xPos;
                   
(property) PaintOptions.xPos?: number | undefined
  let yPos = opts.yPos;
                   
(property) PaintOptions.yPos?: number | undefined
  // ...
}

En JavaScript, incluso si la propiedad nunca se ha configurado, aún podemos acceder a ella; solo nos dará el valor undefined. Podemos manejar undefined especialmente comprobándolo.

Prueba este código ↗

function paintShape(opts: PaintOptions) {
  let xPos = opts.xPos === undefined ? 0 : opts.xPos;
       
let xPos: number
  let yPos = opts.yPos === undefined ? 0 : opts.yPos;
       
let yPos: number
  // ...
}

Ten en cuenta que este patrón de configuración predeterminada para valores no especificados es tan común que JavaScript tiene una sintaxis que lo admite.

Prueba este código ↗

function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
  console.log("x coordinate at", xPos);
                                  
(parameter) xPos: number
  console.log("y coordinate at", yPos);
                                  
(parameter) yPos: number
  // ...
}

Aquí usamos un patrón de desestructuración ↗ para parámetros de paintShape y se proporcionó valores predeterminados ↗ para xPos y yPos. Ahora, xPos y yPos están definitivamente presentes dentro del cuerpo de paintShape, pero son opcionales para cualquiera que llame a paintShape.

Ten en cuenta que actualmente no hay forma de colocar anotaciones tipográficas dentro de patrones de desestructuración. Esto se debe a que la siguiente sintaxis ya significa algo diferente en JavaScript. Pruebe este código ↗

function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
  render(shape);
  render(xPos);
Cannot find name 'xPos'.2304Cannot find name 'xPos'.}
Error generado
Cannot find name 'shape'. Did you mean 'Shape'?

En un patrón de desestructuración de objetos, shape: Shape significa “tomar la propiedad shape y redefinirla localmente como una variable llamada Shape. Del mismo modo xPos: number crea una variable llamada number cuyo valor se basa en el parámetro xPos.

Propiedades readonly

Las propiedades también se pueden marcar como readonly para TypeScript. Si bien no cambiará ningún comportamiento en tiempo de ejecución, no se puede escribir en una propiedad marcada como readonly durante la verificación de tipos.

Prueba este código ↗

interface SomeType {
  readonly prop: string;
}
 
function doSomething(obj: SomeType) {
  // Podemos leer desde 'obj.prop'.
  console.log(`prop has the value '${obj.prop}'.`);
 
  // Pero no lo podemos reasignar.
  obj.prop = "hello";
}
Error generado
Cannot assign to 'prop' because it is a read-only property.

Usar el modificador readonly no necesariamente implica que un valor sea totalmente inmutable - o en otras palabras, que su contenido interno no se pueda cambiar. Simplemente significa que no se puedes reescribir la propiedad en sí.

Prueba este código ↗

interface Home {
  readonly resident: { name: string; age: number };
}
 
function visitForBirthday(home: Home) {
  // Podemos leer y editar propiedadesde de 'home.resident'.
  console.log(`Happy birthday ${home.resident.name}!`);
  home.resident.age++;
}
 
function evict(home: Home) {
  // Pero no podemos escribir en la propiedad 'resident' misma de 'Home'.
  home.resident = {
    name: "Victor the Evictor",
    age: 42,
  };
}
Error generado
Cannot assign to 'resident' because it is a read-only property.

Es importante gestionar las expectativas de lo que implica readonly. Es útil señalar la intención durante el tiempo de desarrollo de TypeScript sobre cómo se debe usar un objeto. TypeScript no tiene en cuenta si las propiedades de dos tipos son readonly al comprobar si esos tipos son compatibles, por lo que las propiedades readonly también pueden cambiar mediante alias.

Prueba este código ↗

interface Person {
  name: string;
  age: number;
}
 
interface ReadonlyPerson {
  readonly name: string;
  readonly age: number;
}
 
let writablePerson: Person = {
  name: "Person McPersonface",
  age: 42,
};
 
// Funciona
let readonlyPerson: ReadonlyPerson = writablePerson;
 
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'

Usando modificadores de mapeo, puedes eliminar atributos readonly.

Firmas de índices

A veces no conoces todos los nombres de las propiedades de un tipo de antemano, pero sí conoces la forma de los valores.

En esos casos puedes usar una firma de índice para describir los tipos de valores posibles, por ejemplo:

Prueba este código ↗

interface StringArray {
  [index: number]: string;
}
 
const myArray: StringArray = getStringArray();
const secondItem = myArray[1];
          
const secondItem: string

Arriba, tenemos una interfaz StringArray que tiene una firma de índice. Esta firma de índice establece que cuando un StringArray se indexa con un number, devolverá un string.

Solo se permiten algunos tipos para las propiedades de firma de índice: string, number, symbol, patrones de cadena de plantilla y tipos de unión que constan solo de estos.

Si bien las firmas de índice de cadenas son una forma poderosa de describir el patrón de “diccionario”, también exigen que todas las propiedades coincidan con su tipo de retorno. Esto se debe a que un índice de cadena declara que obj.property también está disponible como obj["property"]. En el siguiente ejemplo, el tipo de name no coincide con el tipo del índice de cadena y el verificador de tipos da un error:

Prueba este código ↗

interface NumberDictionary {
  [index: string]: number;
 
  length: number; // ok
  name: string;
}
Error generado
Property 'name' of type 'string' is not assignable to 'string' index type 'number'.

Sin embargo, las propiedades de diferentes tipos son aceptables si la firma del índice es una unión de los tipos de propiedad:

Prueba este código ↗

interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number; // ok, length is a number
  name: string; // ok, name is a string
}

Finalmente, puedes hacer que las firmas de índice sean readonly para evitar la asignación a sus índices:

Prueba este código ↗

interface ReadonlyStringArray {
  readonly [index: number]: string;
}
 
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
Error generado
Index signature in type 'ReadonlyStringArray' only permits reading.

No puedes asignar en myArray[2] porque la firma del índice es readonly.

Chequeos de propiedad en exceso

Dónde y cómo se le asigna un tipo a un objeto puede marcar la diferencia en el sistema de tipos. Uno de los ejemplos clave de esto es la verificación excesiva de propiedades, que valida el objeto más exhaustivamente cuando se crea y se asigna a un tipo de objeto durante la creación.

Prueba este código ↗

interface SquareConfig {
  color?: string;
  width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
  return {
    color: config.color || "red",
    area: config.width ? config.width * config.width : 20,
  };
}
 
let mySquare = createSquare({ colour: "red", width: 100 });
Error generado
Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'.
  Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?

Observa que el argumento dado para createSquare se escribe colour en lugar de color. En JavaScript simple, este tipo de cosas falla silenciosamente.

Podrías argumentar que este programa está escrito correctamente, ya que las propiedades width son compatibles, no hay ninguna propiedad colour presente y la propiedad color adicional es insignificante.

Sin embargo, TypeScript adopta la postura de que probablemente haya un error en este código. Los objetos literales reciben un tratamiento especial y se someten a una verificación excesiva de propiedades cuando se asignan a otras variables o se pasan como argumentos. Si un objeto literal tiene propiedades que el “tipo de destino” no tiene, obtendrás un error:

Prueba este código ↗

let mySquare = createSquare({ colour: "red", width: 100 });
Error generado
Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'.
  Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?

Sortear estos controles es realmente sencillo. El método más sencillo es simplemente utilizar una aserción de tipo:

Prueba este código ↗

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

Sin embargo, un mejor enfoque podría ser agregar una firma de índice de cadena si estás seguro de que el objeto puede tener algunas propiedades adicionales que se usan de alguna manera especial. Si SquareConfig puede tener propiedades color y width con los tipos anteriores, pero también puede tener cualquier cantidad de otras propiedades, entonces podríamos definirlo así:

Prueba este código ↗

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}

Aquí estamos diciendo que SquareConfig puede tener cualquier cantidad de propiedades, y siempre que no sean color o width, sus tipos no importan.

Una última forma de evitar estas comprobaciones, que puede resultar un poco sorprendente, es asignar el objeto a otra variable: Dado que la asignación de squareOptions no se someterá a controles excesivos de propiedades, el compilador no te dará un error:

Prueba este código ↗

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

La solución anterior funcionará siempre que tengas una propiedad común entre squareOptions y SquareConfig. En este ejemplo, era la propiedad width. Sin embargo, fallará si la variable no tiene ninguna propiedad de objeto común. Por ejemplo:

Prueba este código ↗

let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions);
Error generado
Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.

Ten en cuenta que para un código simple como el anterior, probablemente no deberías intentar “eludir” estas comprobaciones. Para literales de objetos más complejos que tienen métodos y mantienen el estado, es posible que debas tener en cuenta estas técnicas, pero la mayoría de los errores de propiedad excesivos son en realidad bugs.

Eso significa que si tienes problemas excesivos en la verificación de propiedades para algo, es posible que debas revisar algunas de tus declaraciones de tipo. En este caso, si está bien pasar un objeto con una propiedad color o colour a createSquare, debes corregir la definición de SquareConfig para reflejar eso.

Tipos extendidos

Es bastante común tener tipos que podrían ser versiones más específicas de otros tipos. Por ejemplo, podríamos tener un tipo BasicAddress que describa los campos necesarios para enviar cartas y paquetes en los EE. UU.

Prueba este código ↗

interface BasicAddress {
  name?: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}

En algunas situaciones eso es suficiente, pero las direcciones a menudo tienen un número de unidad asociado si el edificio en una dirección tiene varias unidades. Luego podemos describir una AddressWithUnit.

Prueba este código ↗

interface AddressWithUnit {
  name?: string;
  unit: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}

Esto hace el trabajo, pero la desventaja aquí es que tuvimos que repetir todos los demás campos de BasicAddress cuando nuestros cambios eran puramente aditivos. En su lugar, podemos ampliar el tipo BasicAddress original y simplemente agregar los nuevos campos que son exclusivos de AddressWithUnit.

Prueba este código ↗

interface BasicAddress {
  name?: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}
 
interface AddressWithUnit extends BasicAddress {
  unit: string;
}

La palabra clave extends en una interface nos permite copiar efectivamente miembros de otros tipos con nombre y agregar los nuevos miembros que queramos. Esto puede ser útil para reducir la cantidad de declaraciones de tipo repetitivas que tenemos que escribir y para señalar la intención de que varias declaraciones diferentes de la misma propiedad podrían estar relacionadas. Por ejemplo, AddressWithUnit no necesitaba repetir la propiedad street y debido a que street se origina en BasicAddress, el lector sabrá que esos dos tipos están relacionados de alguna manera.

Las “interfaces” también pueden extenderse desde múltiples tipos.

Prueba este código ↗

interface Colorful {
  color: string;
}
 
interface Circle {
  radius: number;
}
 
interface ColorfulCircle extends Colorful, Circle {}
 
const cc: ColorfulCircle = {
  color: "red",
  radius: 42,
};

Tipos de intersección

La interface nos permitió crear nuevos tipos a partir de otros tipos extendiéndolos. TypeScript proporciona otra construcción llamada tipos de intersección que se utiliza principalmente para combinar tipos de objetos existentes.

Un tipo de intersección se define usando el operador &.

Prueba este código ↗

interface Colorful {
  color: string;
}
interface Circle {
  radius: number;
}
 
type ColorfulCircle = Colorful & Circle;

Aquí, hemos cruzado Colorful y Circle para producir un nuevo tipo que tiene todos los miembros de Colorful y Circle.

Prueba este código ↗

function draw(circle: Colorful & Circle) {
  console.log(`Color was ${circle.color}`);
  console.log(`Radius was ${circle.radius}`);
}
 
// okay
draw({ color: "blue", radius: 42 });
 
// oops
draw({ color: "red", raidus: 42 });
Error generado
Argument of type '{ color: string; raidus: number; }' is not assignable to parameter of type 'Colorful & Circle'.
  Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?

Interfaces vs. Intersecciones

Acabamos de ver dos formas de combinar tipos que son similares, pero que en realidad son sutilmente diferentes. Con las interfaces, podríamos usar una cláusula extends para extendernos desde otros tipos, y pudimos hacer algo similar con las intersecciones y nombrar el resultado con un alias de tipo. La principal diferencia entre los dos es cómo se manejan los conflictos, y esa diferencia suele ser una de las razones principales por las que elegirías uno sobre el otro entre una interfaz y un alias de tipo de intersección.

Tipos de objetos generics

Imaginemos un tipo Box que puede contener cualquier valor: strings, numbers, Jirafas, lo que sea.

Prueba este código ↗

interface Box {
  contents: any;
}

En este momento, la propiedad content está tipada como any, lo cual funciona, pero puede provocar accidentes en el futuro.

En su lugar podríamos usar unknown, pero eso significaría que en los casos en los que ya conocemos el tipo de contents, necesitaríamos realizar comprobaciones de precaución o usar aserciones de tipo propensas a errores.

Prueba este código ↗

interface Box {
  contents: unknown;
}
 
let x: Box = {
  contents: "hello world",
};
 
// podemos verificar 'x.contents'
if (typeof x.contents === "string") {
  console.log(x.contents.toLowerCase());
}
 
// o podemos utilizar aserciones de tipos
console.log((x.contents as string).toLowerCase());

Un enfoque seguro sería, en su lugar, crear diferentes tipos de Box para cada tipo de contents.

Prueba este código ↗

interface NumberBox {
  contents: number;
}
 
interface StringBox {
  contents: string;
}
 
interface BooleanBox {
  contents: boolean;
}

Pero eso significa que tendremos que crear diferentes funciones, o sobrecargas de funciones, para operar en estos tipos.

Prueba este código ↗

function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
  box.contents = newContents;
}

Eso es un montón de repeticiones. Además, es posible que más adelante necesitemos introducir nuevos tipos y sobrecargas. Esto es frustrante, ya que nuestros tipos de box y sobrecargas son todos iguales.

En su lugar, podemos crear un tipo genérico Box que declare un parámetro de tipo.

Prueba este código ↗

interface Box<Type> {
  contents: Type;
}

Podrías leer esto como “Un Box de Type es algo cuyo contents tiene el tipo Type”. Más adelante, cuando nos referimos a Box, tenemos que dar un argumento de tipo en lugar de Type.

Prueba este código ↗

let box: Box<string>;

Piensa en Box como una plantilla para un tipo real, donde Type es un marcador de posición que será reemplazado por algún otro tipo. Cuando TypeScript ve Box<string>, reemplazará cada instancia de Type en Box<Type> con string y terminará trabajando con algo como { content: string }. En otras palabras, Box<string> y nuestro StringBox anterior funcionan de manera idéntica.

Prueba este código ↗

interface Box<Type> {
  contents: Type;
}
interface StringBox {
  contents: string;
}
 
let boxA: Box<string> = { contents: "hello" };
boxA.contents;
        
(property) Box<string>.contents: string
 
let boxB: StringBox = { contents: "world" };
boxB.contents;
        
(property) StringBox.contents: string

Box es reutilizable en el sentido de que Type se puede sustituir por cualquier cosa. Eso significa que cuando necesitamos un Box para un nuevo tipo, no necesitamos declarar un nuevo tipo Box en absoluto (aunque ciertamente podríamos hacerlo si quisiéramos).

Prueba este código ↗

interface Box<Type> {
  contents: Type;
}
 
interface Apple {
  // ....
}
 
// Igual a '{ contents: Apple }'.
type AppleBox = Box<Apple>;

Esto también significa que podemos evitar las sobrecargas por completo usando funciones genéricas.

Prueba este código ↗

function setContents<Type>(box: Box<Type>, newContents: Type) {
  box.contents = newContents;
}

Vale la pena señalar que los alias de tipo también pueden ser generics. Podríamos haber definido nuestra nueva interfaz Box<Type>, que sería:

Prueba este código ↗

interface Box<Type> {
  contents: Type;
}

usando un alias de tipo en su lugar:

Prueba este código ↗

type Box<Type> = {
  contents: Type;
};

Dado que los alias de tipos, a diferencia de las interfaces, pueden describir más que solo tipos de objetos, también podemos usarlos para escribir otros tipos de tipos auxiliares genéricos.

Prueba este código ↗

type OrNull<Type> = Type | null;
 
type OneOrMany<Type> = Type | Type[];
 
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
           
type OneOrManyOrNull<Type> = OneOrMany<Type> | null
 
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
               
type OneOrManyOrNullStrings = OneOrMany<string> | null

Volveremos a los alias de tipo en un momento.

El tipo Array

Los tipos de objetos generics suelen ser algún tipo de tipo de contenedor que funciona independientemente del tipo de elementos que contienen. Es ideal que las estructuras de datos funcionen de esta manera para que sean reutilizables en diferentes tipos de datos.

Resulta que hemos estado trabajando con un tipo como ese a lo largo de este manual: el tipo Array. Siempre que escribimos tipos como number[] o string[], en realidad es solo una abreviatura de Array<number> y Array<string>.

Prueba este código ↗

function doSomething(value: Array<string>) {
  // ...
}
 
let myArray: string[] = ["hello", "world"];
 
// cualquiera de estos funciona!
doSomething(myArray);
doSomething(new Array("hello", "world"));

Al igual que el tipo Box anterior, Array en sí es un tipo genérico.

Prueba este código ↗

interface Array<Type> {
  /**
   * Obtiene o asigna la longitud del array.
   */
  length: number;
 
  /**
   * Remueve el último elemento del array y lo devuelve.
   */
  pop(): Type | undefined;
 
  /**
   * Añade un nuevo elemento al array, y retorna la nueva longitud del array.
   */
  push(...items: Type[]): number;
 
  // ...
}

JavaScript moderno también proporciona otras estructuras de datos que son genéricas, como Map<K, V>, Set<T> y Promise<T>. Todo lo que esto realmente significa es que debido a cómo se comportan Map, Set y Promise, pueden funcionar con cualquier conjunto de tipos.

El tipo ReadonlyArray

ReadonlyArray es un tipo especial que describe arrays que no deben cambiarse.

Prueba este código ↗

function doStuff(values: ReadonlyArray<string>) {
  // Podemos leer desde 'values'...
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
 
  // ...pero no podemos mutar 'values'.
  values.push("hello!");
}
Error generado
Property 'push' does not exist on type 'readonly string[]'.

Al igual que el modificador readonly para propiedades, es principalmente una herramienta que podemos usar con la siguiente intención. Cuando vemos una función que devuelve ReadonlyArrays, nos dice que no debemos cambiar el contenido en absoluto, y cuando vemos una función que consume ReadonlyArrays, nos dice que podemos pasar cualquier array a esa función sin preocuparnos de que cambie su contenido.

A diferencia de Array, no existe un constructor ReadonlyArray que podamos usar.

Prueba este código ↗

new ReadonlyArray("red", "green", "blue");
Error generado
'ReadonlyArray' only refers to a type, but is being used as a value here.

En su lugar, podemos asignar Arrays normales a ReadonlyArrays.

Prueba este código ↗

const roArray: ReadonlyArray<string> = ["red", "green", "blue"];

Así como TypeScript proporciona una sintaxis abreviada para Array<Type> con Type[], también proporciona una sintaxis abreviada para ReadonlyArray<Type> con readonly Type[] .

Prueba este código ↗

function doStuff(values: readonly string[]) {
  // Podemos leer desde 'values'...
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
 
  // ...pero no podemos mutar 'values'.
  values.push("hello!");
}
Error generado
Property 'push' does not exist on type 'readonly string[]'.

Una última cosa a tener en cuenta es que, a diferencia del modificador de propiedad readonly, la asignabilidad no es bidireccional entre Arrays y ReadonlyArrays normales.

Prueba este código ↗

let x: readonly string[] = [];
let y: string[] = [];
 
x = y;
y = x;
Error generado
The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.

Tipos de tuplas

Un tipo tupla es otro tipo de tipo Array que sabe exactamente cuántos elementos contiene y exactamente qué tipos contiene en posiciones específicas.

Prueba este código ↗

type StringNumberPair = [string, number];

Aquí, StringNumberPair es un tipo de tupla de string y number. Al igual que ReadonlyArray, no tiene representación en tiempo de ejecución, pero es importante para TypeScript. Para el sistema de tipos, StringNumberPair describe arrays cuyo índice 0 contiene un string y cuyo índice 1 contiene un number.

Prueba este código ↗

function doSomething(pair: [string, number]) {
  const a = pair[0];
       
const a: string
  const b = pair[1];
       
const b: number
  // ...
}
 
doSomething(["hello", 42]);

Si intentamos indexar más allá del número de elementos, obtendremos un error.

Prueba este código ↗

function doSomething(pair: [string, number]) {
  // ...
 
  const c = pair[2];
}
Error generado
Tuple type '[string, number]' of length '2' has no element at index '2'.

También podemos deestructurar tuplas ↗ usando la desestructuración de arrays de JavaScript.

Prueba este código ↗

function doSomething(stringHash: [string, number]) {
  const [inputString, hash] = stringHash;
 
  console.log(inputString);
                  
const inputString: string
 
  console.log(hash);
               
const hash: number
}

Los tipos de tupla son útiles en API fuertemente basadas en convenciones, donde el significado de cada elemento es “obvio”. Esto nos da flexibilidad en el nombre que queramos darle a nuestras variables cuando las desestructuramos. En el ejemplo anterior, pudimos nombrar los elementos 0 y 1 como quisimos. Sin embargo, dado que no todos los usuarios tienen la misma visión de lo que es obvio, puede valer la pena reconsiderar si usar objetos con nombres de propiedad descriptivos puede ser mejor para tu API.

Aparte de esas comprobaciones de longitud, los tipos de tupla simples como estos son equivalentes a tipos que son versiones de Arrays que declaran propiedades para índices específicos y que declaran length con un tipo literal numérico.

Prueba este código ↗

interface StringNumberPair {
  // propiedades especializadas
  length: 2;
  0: string;
  1: number;
 
  // Otros miembros 'Array<string | number>' ...
  slice(start?: number, end?: number): Array<string | number>;
}

Otra cosa que te puede interesar es que las tuplas pueden tener propiedades opcionales escribiendo un signo de interrogación (? después del tipo de elemento). Los elementos de tupla opcionales solo pueden aparecer al final y también afectan el tipo de length.

Prueba este código ↗

type Either2dOr3d = [number, number, number?];
 
function setCoordinate(coord: Either2dOr3d) {
  const [x, y, z] = coord;
              
const z: number | undefined
 
  console.log(`Provided coordinates had ${coord.length} dimensions`);
                                                  
(property) length: 2 | 3
}

Las tuplas también pueden tener elementos rest, que tienen que ser de tipo array/tupla.

Prueba este código ↗

type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
  • StringNumberBooleans describe una tupla cuyos dos primeros elementos son string y number respectivamente, pero que puede tener cualquier número de booleans siguientes.
  • StringBooleansNumber describe una tupla cuyo primer elemento es string y luego cualquier número de booleans y termina con un number.
  • BooleansStringNumber describe una tupla cuyos elementos iniciales son cualquier número de booleans y terminan con un string y luego un number.

Una tupla con un elemento rest no tiene un length establecido; solo tiene un conjunto de elementos conocidos en diferentes posiciones.

Prueba este código ↗

const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];

¿Por qué podrían ser útiles los elementos opcionales y elementos rest? Bueno, permite que TypeScript corresponda tuplas con listas de parámetros. Los tipos de tuplas se pueden usar en parámetros y argumentos rest, de modo que lo siguiente:

Prueba este código ↗

function readButtonInput(...args: [string, number, ...boolean[]]) {
  const [name, version, ...input] = args;
  // ...
}

básicamente equivale a:

Prueba este código ↗

function readButtonInput(name: string, version: number, ...input: boolean[]) {
  // ...
}

Esto es útil cuando quieres tomar una cantidad variable de argumentos con un parámetro rest y necesitas una cantidad mínima de elementos, pero no quieres introducir variables intermedias.

Tipos de tuplas readonly

Una nota final sobre los tipos de tupla: los tipos de tupla tienen variantes de solo lectura y se pueden especificar colocando un modificador readonly delante de ellos, al igual que con la sintaxis abreviada de array.

Prueba este código ↗

function doSomething(pair: readonly [string, number]) {
  // ...
}

Como es de esperar, escribir en cualquier propiedad de una tupla readonly no está permitido en TypeScript.

Prueba este código ↗

function doSomething(pair: readonly [string, number]) {
  pair[0] = "hello!";
}
Error generado
Cannot assign to '0' because it is a read-only property.

Las tuplas tienden a crearse y no modificarse en la mayoría del código, por lo que anotar tipos como tuplas readonly cuando sea posible es una buena opción predeterminada. Esto también es importante dado que los literales de array con aserciones const se inferirán con tipos de tupla readonly.

Prueba este código ↗

let point = [3, 4] as const;
 
function distanceFromOrigin([x, y]: [number, number]) {
  return Math.sqrt(x ** 2 + y ** 2);
}
 
distanceFromOrigin(point);
Error generado
Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'.
  The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.

Aquí, distanceFromOrigin nunca modifica sus elementos, pero espera una tupla mutable. Dado que el tipo de point se dedujo como readonly [3, 4], no será compatible con [number, number], ya que ese tipo no puede garantizar que los elementos de point no sean mutados.

Última actualización