Capítulo 4. Objetos

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Literales de objeto

Un conjunto de claves y valores

Cada uno con su propio tipo

Enel capítulo 3, "Uniones y literales", se desarrollaron los tipos unión y literal: cómo trabajar con primitivas como boolean y con valores literales de las mismas como true. Esas primitivas sólo arañan la superficie de las complejas formas de objeto que el código JavaScript utiliza habitualmente. TypeScript sería bastante inutilizable si no fuera capaz de representar esos objetos. Este capítulo tratará sobre cómo describir formas de objeto complejas y cómo TypeScript comprueba su asignabilidad.

Tipos de objetos

Cuando crea un literal de objeto con la sintaxis {...}, TypeScript lo considerará un nuevo tipo de objeto, o forma de tipo, basándose en sus propiedades. Ese tipo de objeto tendrá los mismos nombres de propiedades y tipos primitivos que los valores del objeto. El acceso a las propiedades del valor puede hacerse con value.member o con la sintaxis equivalente value['member'].

TypeScript entiende que el tipo de la siguiente variable poet es el de un objeto con dos propiedades: born, de tipo number, y name, de tipo string. Acceder a esos miembros estaría permitido, pero intentar acceder a cualquier otro nombre de miembro provocaría un error de tipo por no existir ese nombre:

const poet = {
    born: 1935,
    name: "Mary Oliver",
};

poet['born']; // Type: number
poet.name; // Type: string

poet.end;
//   ~~~
// Error: Property 'end' does not exist on
// type '{ born: number; name: string; }'.

Los tipos de objeto son un concepto básico para que TypeScript entienda el código JavaScript. Cada valor distinto de null y undefined tiene un conjunto de miembros en su forma de tipo de respaldo, por lo que TypeScript debe entender el tipo de objeto de cada valor para poder comprobarlo tipográficamente .

Declarar tipos de objetos

Inferir tipos directamente de objetos existentes está muy bien, pero con el tiempo querrás poder declarar explícitamente el tipo de un objeto. Necesitarás una forma de describir la forma de un objeto separadamente de los objetos que la satisfacen.

Los tipos de objeto pueden describirse utilizando una sintaxis similar a la de los literales de objeto, pero con tipos en lugar de valores para los campos. Es la misma sintaxis que TypeScript muestra en los mensajes de error sobre la asignabilidad de tipos.

Esta variable poetLater es del mismo tipo que las anteriores name: string y born: number:

let poetLater: {
    born: number;
    name: string;
};

// Ok
poetLater = {
    born: 1935,
    name: "Mary Oliver",
};

poetLater = "Sappho";
// Error: Type 'string' is not assignable to
// type '{ born: number; name: string; }'

Tipos de objetos alias

Escribir constantemente en tipos de objetos como { born: number; name: string; } se volvería tedioso con bastante rapidez. Es más habitual utilizar alias de tipos para asignar un nombre a cada forma de tipo.

El fragmento de código anterior podría reescribirse con un type Poet, que tiene la ventaja añadida de hacer que el mensaje de error de asignabilidad de TypeScript sea un poco más directo y legible:

type Poet = {
    born: number;
    name: string;
};

let poetLater: Poet;

// Ok
poetLater = {
    born: 1935,
    name: "Sara Teasdale",
};

poetLater = "Emily Dickinson";
// Error: Type 'string' is not assignable to type 'Poet'.
Nota

La mayoría de los proyectos TypeScript prefieren utilizar la palabra clave interface para describir los tipos de objeto, que es una característica que no trataré hasta el Capítulo 7, "Interfaces". Los tipos de objeto con alias y las interfaces soncasi idénticos: todo lo que se dice en este capítulo se aplica también a las interfaces.

Traigo a colación estos tipos de objeto ahora porque entender cómo TypeScript interpreta los literales de objeto es una parte importante del aprendizaje del sistema de tipos de TypeScript. Estos conceptos seguirán siendo importantes una vez que pasemos a las características en la siguiente sección de este libro.

Tipificación estructural

El sistema de tipos de TypeScript es de tipo estructural: es decir, cualquier valor que satisfaga un tipo puede utilizarse como valor de ese tipo. En otras palabras, cuando declaras que un parámetro o variable es de un tipo de objeto concreto, le estás diciendo a TypeScript que sea cual sea el objeto u objetos que utilices, tienen que tener esas propiedades.

Los siguientes tipos de objeto alias WithFirstName y WithLastName sólo declaran un único miembro de tipo string. La variable hasBoth resulta que tiene ambos -aunque no se haya declarado como tal explícitamente-, por lo que se puede proporcionar a variables declaradas como cualquiera de los dos tipos de objeto alias:

type WithFirstName = {
  firstName: string;
};

type WithLastName = {
  lastName: string;
};

const hasBoth = {
  firstName: "Lucille",
  lastName: "Clifton",
};

// Ok: `hasBoth` contains a `firstName` property of type `string`
let withFirstName: WithFirstName = hasBoth;

// Ok: `hasBoth` contains a `lastName` property of type `string`
let withLastName: WithLastName = hasBoth;

La mecanografía estructural no es lo mismo que la mecanografía pato, que viene de la frase "Si parece un pato y grazna como un pato, probablemente sea un pato".

  • Se habla de tipado estructural cuando existe un sistema estático que comprueba el tipo -en el caso de TypeScript, el comprobador de tipos-.

  • La tipificación agachada es cuando nada comprueba los tipos de objetos hasta que se utilizan en tiempo de ejecución.

En resumen: JavaScript es de tipado duck mientras que TypeScript es de tipado estructural.

Comprobación de uso

Cuando proporciona un valor a una ubicación anotada con un tipo de objeto, TypeScript comprobará que el valor es asignable a ese tipo de objeto. Para empezar, el valor debe tener las propiedades requeridas del tipo de objeto. Si falta en el objeto algún miembro requerido en el tipo de objeto, TypeScript emitirá un error de tipo.

El siguiente tipo de objeto alias FirstAndLastNames requiere que existan las propiedades first y last. Un objeto que contenga ambas se puede utilizar en una variable declarada de tipo FirstAndLastNames, pero un objeto sin ellas no:

type FirstAndLastNames = {
  first: string;
  last: string;
};

// Ok
const hasBoth: FirstAndLastNames = {
  first: "Sarojini",
  last: "Naidu",
};

const hasOnlyOne: FirstAndLastNames = {
  first: "Sappho"
};
// Property 'last' is missing in type '{ first: string; }'
// but required in type 'FirstAndLastNames'.

Tampoco se permiten tipos no coincidentes entre ambos. Los tipos de objeto especifican tanto los nombres de las propiedades requeridas como los tipos que se espera que sean esas propiedades. Si la propiedad de un objeto no coincide, TypeScript informará de un error de tipo.

El siguiente tipo TimeRange espera que el miembro start sea del tipo Date. El objeto hasStartString está provocando un error de tipo porque su start es del tipo string en su lugar:

type TimeRange = {
  start: Date;
};

const hasStartString: TimeRange = {
  start: "1879-02-13",
  // Error: Type 'string' is not assignable to type 'Date'.
};

Comprobación de bienes sobrantes

TypeScript informará de un error de tipo si se declara una variable con un tipo de objeto y su valor inicial tiene más campos de los que describe su tipo. Por tanto, declarar una variable con un tipo de objeto es una forma de conseguir que el comprobador de tipos se asegure de que sólo tiene los campos esperados de ese tipo.

La siguiente variable poetMatch tiene exactamente los campos descritos en el tipo de objeto aliasado por Poet, mientras que extraProperty provoca un error de tipo por tener unapropiedad extra:

type Poet = {
    born: number;
    name: string;
}

// Ok: all fields match what's expected in Poet
const poetMatch: Poet = {
  born: 1928,
  name: "Maya Angelou"
};

const extraProperty: Poet = {
    activity: "walking",
    born: 1935,
    name: "Mary Oliver",
};
// Error: Type '{ activity: string; born: number; name: string; }'
// is not assignable to type 'Poet'.
//   Object literal may only specify known properties,
//   and 'activity' does not exist in type 'Poet'.

Ten en cuenta que las comprobaciones de exceso de propiedades sólo se activan para los literales de objeto que se crean en ubicaciones declaradas como tipo de objeto. Proporcionar un literal de objeto existente evita las comprobaciones de exceso de propiedades.

Esta variable extraPropertyButOk no provoca un error de tipo con el tipo Poet del ejemplo anterior porque su valor inicial coincide estructuralmente con Poet:

const existingObject = {
    activity: "walking",
    born: 1935,
    name: "Mary Oliver",
};

const extraPropertyButOk: Poet = existingObject; // Ok

Las comprobaciones de exceso de propiedades se activarán en cualquier lugar en el que se esté creando un nuevo objeto en una ubicación que espere que coincida con un tipo de objeto -lo que, como verás en capítulos posteriores, incluye miembros de matrices, campos de clases y parámetros de funciones. Prohibir el exceso de propiedades es otra forma en la que TypeScript te ayuda a asegurarte de que tu código es limpio y hace lo que esperas. El exceso de propiedades no declaradas en sus tipos de objeto suelen ser nombres de propiedades mal escritos o código no utilizado.

Tipos de objetos anidados

Como los objetos JavaScript pueden anidarse como miembros de otros objetos, los tipos de objeto de TypeScript deben poder representar tipos de objeto anidados en el sistema de tipos. La sintaxis para hacerlo es la misma que antes, pero con un tipo de objeto { ... } en lugar de un nombre primitivo.

Poem se declara que el tipo es un objeto cuya propiedad author tiene firstName: string y lastName: string. La variable poemMatch es asignable a Poem porque coincide con esa estructura, mientras que poemMismatch no lo es porque su propiedad author incluye name en lugar de firstName y lastName:

type Poem = {
    author: {
        firstName: string;
        lastName: string;
    };
    name: string;
};

// Ok
const poemMatch: Poem = {
    author: {
        firstName: "Sylvia",
        lastName: "Plath",
    },
    name: "Lady Lazarus",
};

const poemMismatch: Poem = {
    author: {
        name: "Sylvia Plath",
    },
    // Error: Type '{ name: string; }' is not assignable
    // to type '{ firstName: string; lastName: string; }'.
    //   Object literal may only specify known properties, and 'name'
    //   does not exist in type '{ firstName: string; lastName: string; }'.
    name: "Tulips",
};

Otra forma de escribir type Poem sería extraer la forma de la propiedad author en su propio alias de tipo de objeto, Author. Extraer los tipos anidados en sus propios alias de tipo también ayuda a TypeScript a dar mensajes de error de tipo más informativos. En este caso, puede decir 'Author' en lugar de '{ firstName: string; lastName: string; }':

type Author = {
    firstName: string;
    lastName: string;
};

type Poem = {
    author: Author;
    name: string;
};

const poemMismatch: Poem = {
    author: {
        name: "Sylvia Plath",
    },
    // Error: Type '{ name: string; }' is not assignable to type 'Author'.
    //     Object literal may only specify known properties,
    //     and 'name' does not exist in type 'Author'.
    name: "Tulips",
};
Consejo

Por lo general, es una buena idea trasladar los tipos de objeto anidados a su propio nombre de tipo de esta forma, tanto para que el código sea más legible como para que los mensajes de error de TypeScript sean más legibles.

En capítulos posteriores verás cómo los miembros de un tipo de objeto pueden ser de otros tipos, como matrices y funciones .

Propiedades opcionales

Las propiedades de tipo de objeto no tienen por qué ser todas obligatorias en el objeto. Puedes incluir un ? antes del : en la anotación de tipo de una propiedad de tipo para indicar que se trata de una propiedad opcional.

Este tipo Book sólo requiere una propiedad pages y opcionalmente permite una author. Los objetos que se adhieran a él pueden proporcionar author u omitirla siempre que proporcionen pages:

type Book = {
  author?: string;
  pages: number;
};

// Ok
const ok: Book = {
    author: "Rita Dove",
    pages: 80,
};

const missing: Book = {
    author: "Rita Dove",
};
// Error: Property 'pages' is missing in type
// '{ author: string; }' but required in type 'Book'.

Ten en cuenta que existe una diferencia entre las propiedades opcionales y las propiedades cuyo tipo incluye casualmente undefined en una unión de tipos. Una propiedad declarada como opcional con ? puede no existir. Una propiedad declarada como obligatoria con | undefined debe existir, aunque su valor sea undefined.

La propiedad editor del siguiente tipo Writers puede omitirse al declarar variables porque tiene un ? en su declaración. La propiedad author no tiene un ?, por lo que debe existir, aunque su valor sea sólo undefined:

type Writers = {
  author: string | undefined;
  editor?: string;
};

// Ok: author is provided as undefined
const hasRequired: Writers = {
  author: undefined,
};

const missingRequired: Writers = {};
//    ~~~~~~~~~~~~~~~
// Error: Property 'author' is missing in type
// '{}' but required in type 'Writers'.

El Capítulo7, "Interfaces", tratará más sobre otros tipos de propiedades, mientras que el Capítulo 13, "Opciones de configuración", describirá los ajustes de rigor de TypeScript en torno a propiedades opcionales.

Uniones de tipos de objeto

Es razonable que en el código TypeScript quieras ser capaz de describir un tipo que puede ser uno o más tipos de objeto diferentes que tienen propiedades ligeramente distintas. Además, es posible que tu código quiera ser capaz de estrechar el tipo entre esos tipos de objeto basándose en el valor de una propiedad.

Uniones de tipo objeto inferidas

Si a una variable se le da un valor inicial que podría ser uno de varios tipos de objeto, TypeScript deducirá que su tipo es una unión de tipos de objeto. Ese tipo de unión tendrá un constituyente para cada una de las posibles formas de objeto. Cada una de las posibles propiedades del tipo estará presente en cada uno de esos constituyentes, aunque serán ? tipos opcionales en cualquier tipo que no tenga un valor inicial para ellas.

Este valor poem siempre tiene una propiedad name de tipo string, y puede o no tener propiedades pages y rhymes:

const poem = Math.random() > 0.5
  ? { name: "The Double Image", pages: 7 }
  : { name: "Her Kind", rhymes: true };
// Type:
// {
//   name: string;
//   pages: number;
//   rhymes?: undefined;
// }
// |
// {
//   name: string;
//   pages?: undefined;
//   rhymes: boolean;
// }

poem.name; // string
poem.pages; // number | undefined
poem.rhymes; // boolean | undefined

Uniones explícitas de tipo objeto

Alternativamente, puedes ser más explícito sobre tus tipos de objeto siendo explícito con tu propia unión de tipos de objeto. Hacerlo requiere escribir un poco más de código, pero tiene la ventaja de darte más control sobre tus tipos de objeto. En particular, si el tipo de un valor es una unión de tipos de objeto, el sistema de tipos de TypeScript sólo permitirá acceder a las propiedades que existan en todos esos tipos de unión.

Esta versión de la variable anterior poem está explícitamente tipada para ser un tipo de unión que siempre tiene la propiedad name junto con pages o rhymes. Se permite acceder a name porque siempre existe, pero no se garantiza que pages y rhymes existan:

type PoemWithPages = {
    name: string;
    pages: number;
};

type PoemWithRhymes = {
    name: string;
    rhymes: boolean;
};

type Poem = PoemWithPages | PoemWithRhymes;

const poem: Poem = Math.random() > 0.5
  ? { name: "The Double Image", pages: 7 }
  : { name: "Her Kind", rhymes: true };

poem.name; // Ok

poem.pages;
//   ~~~~~
// Property 'pages' does not exist on type 'Poem'.
//   Property 'pages' does not exist on type 'PoemWithRhymes'.

poem.rhymes;
//   ~~~~~~
// Property 'rhymes' does not exist on type 'Poem'.
//   Property 'rhymes' does not exist on type 'PoemWithPages'.

Restringir el acceso a miembros potencialmente inexistentes de los objetos puede ser bueno para la seguridad del código. Si un valor puede ser de varios tipos, no se garantiza que las propiedades que no existen en todos esos tipos existan en el objeto.

Del mismo modo que las uniones de tipos literales y/o primitivos deben estrecharse de tipo para acceder a propiedades que no existen en todos los constituyentes de tipo, tendrás que estrechar esas uniones de tipo objeto .

Limitación de los tipos de objetos

Si el comprobador de tipos de ve que un área de código sólo puede ejecutarse si un valor tipado en unión contiene una determinada propiedad, estrechará el tipo del valor sólo a los constituyentes que contengan esa propiedad. En otras palabras, el estrechamiento de tipos de TypeScript se aplicará a los objetos si compruebas su forma en el código.

Continuando con el ejemplo de poem explícitamente tipado, comprueba si "pages" in poem actúa como un protector de tipo para TypeScript para indicar que es un PoemWithPages. Si poem no es un PoemWithPages, entonces debe ser un PoemWithRhymes:

if ("pages" in poem) {
    poem.pages; // Ok: poem is narrowed to PoemWithPages
} else {
    poem.rhymes; // Ok: poem is narrowed to PoemWithRhymes
}

Ten en cuenta que TypeScript no permite comprobaciones de existencia de veracidad como if (poem.pages). Intentar acceder a una propiedad de un objeto que podría no existir se considera un error de tipo, aunque se utilice de una forma que parezca comportarse como una guarda de tipo:

if (poem.pages) { /* ... */ }
//       ~~~~~
// Property 'pages' does not exist on type 'Poem'.
//   Property 'pages' does not exist on type 'PoemWithRhymes'.

Sindicatos discriminados

Otra forma popular de unión tipada de objetos en JavaScript y TypeScript es hacer que una propiedad del objeto indique qué forma tiene el objeto. Este tipo de unión tipada se denomina unión discriminada, y la propiedad cuyo valor indica el tipo del objeto es un discriminante. TypeScript es capaz de realizar estrechamientos de tipo para el código que guarda el tipo en propiedades discriminantes.

Por ejemplo, el tipo Poem describe un objeto que puede ser un nuevo tipo o un nuevo tipo , y la propiedad indica cuál.PoemWithPages o un nuevo tipo PoemWithRhymes, y la propiedad type indica cuál. Si poem.type es "pages", entonces TypeScript es capaz de inferir que el tipo de poem debe ser PoemWithPages. Sin ese estrechamiento de tipos, no está garantizada la existencia de ninguna de las dos propiedades en el valor:

type PoemWithPages = {
    name: string;
    pages: number;
    type: 'pages';
};

type PoemWithRhymes = {
    name: string;
    rhymes: boolean;
    type: 'rhymes';
};

type Poem = PoemWithPages | PoemWithRhymes;

const poem: Poem = Math.random() > 0.5
  ? { name: "The Double Image", pages: 7, type: "pages" }
  : { name: "Her Kind", rhymes: true, type: "rhymes" };

if (poem.type === "pages") {
    console.log(`It's got pages: ${poem.pages}`); // Ok
} else {
    console.log(`It rhymes: ${poem.rhymes}`);
}

poem.type; // Type: 'pages' | 'rhymes'

poem.pages;
//   ~~~~~
// Error: Property 'pages' does not exist on type 'Poem'.
//   Property 'pages' does not exist on type 'PoemWithRhymes'.

Las uniones discriminadas son mi característica favorita de TypeScript, porque combinan a la perfección un elegante patrón común de JavaScript con el estrechamiento de tipos de TypeScript.El Capítulo 10, "Genéricos" y sus proyectos asociados mostrarán más sobre el uso de uniones discriminadas para datos genéricos operaciones.

Tipos de intersecciones

Los tipos de unión | de TypeScript representan el tipo de un valor que podría ser uno de dos o más tipos diferentes. Del mismo modo que el operador de ejecución | de JavaScript actúa como contrapartida de su operador &, TypeScript permite representar un tipo que es varios tipos a la vez: un tipo de intersección &. Los tipos de intersección se suelen utilizar con tipos de objetos alias para crear un nuevo tipo que combine varios tipos de objetos existentes.

Los siguientes tipos Artwork y Writing se utilizan para formar un tipo combinado WrittenArt que tiene las propiedades genre, name y pages:

type Artwork = {
    genre: string;
    name: string;
};

type Writing = {
    pages: number;
    name: string;
};

type WrittenArt = Artwork & Writing;
// Equivalent to:
// {
//   genre: string;
//   name: string;
//   pages: number;
// }

Tipos de intersección puede combinarse con tipos de unión, lo que a veces resulta útil para describir uniones discriminadas en un solo tipo.

Este tipo ShortPoem siempre tiene una propiedad author, luego también es una unión discriminada en una propiedad type:

type ShortPoem = { author: string } & (
    | { kigo: string; type: "haiku"; }
    | { meter: number; type: "villanelle"; }
);

// Ok
const morningGlory: ShortPoem = {
    author: "Fukuda Chiyo-ni",
    kigo: "Morning Glory",
    type: "haiku",
};

const oneArt: ShortPoem = {
    author: "Elizabeth Bishop",
    type: "villanelle",
};
// Error: Type '{ author: string; type: "villanelle"; }'
// is not assignable to type 'ShortPoem'.
//   Type '{ author: string; type: "villanelle"; }' is not assignable to
//   type '{ author: string; } & { meter: number; type: "villanelle"; }'.
//     Property 'meter' is missing in type '{ author: string; type: "villanelle"; }'
//     but required in type '{ meter: number; type: "villanelle"; }'.

Peligros de los tipos de intersección

Los tipos de intersección son un concepto útil, pero es fácil utilizarlos de forma que te confundan a ti o al compilador de TypeScript. Te recomiendo que intentes mantener el código lo más simple posible cuando los utilices.

Errores de asignación largos

Asignabilidad los mensajes de error de TypeScript se vuelven mucho más difíciles de leer cuando creas tipos de intersección complejos, como uno combinado con un tipo de unión. Éste será un tema común con el sistema de tipos de TypeScript (y con los lenguajes de programación tipados en general): cuanto más complejo sea, más difícil será entender los mensajes del comprobador de tipos.

En el caso del fragmento de código anterior ShortPoem, sería mucho más legible dividir el tipo en una serie de tipos de objeto con alias para permitir que TypeScript imprima esos nombres:

type ShortPoemBase = { author: string };
type Haiku = ShortPoemBase & { kigo: string; type: "haiku" };
type Villanelle = ShortPoemBase & { meter: number; type: "villanelle" };
type ShortPoem = Haiku | Villanelle;

const oneArt: ShortPoem = {
    author: "Elizabeth Bishop",
    type: "villanelle",
};
// Type '{ author: string; type: "villanelle"; }'
// is not assignable to type 'ShortPoem'.
//   Type '{ author: string; type: "villanelle"; }'
//   is not assignable to type 'Villanelle'.
//     Property 'meter' is missing in type
//     '{ author: string; type: "villanelle"; }'
//     but required in type '{ meter: number; type: "villanelle"; }'.

nunca

Los tipos de intersección también son fáciles de utilizar mal y crear con ellos un tipo imposible. Los tipos primitivos no pueden unirse como constituyentes en un tipo de intersección porque es imposible que un valor sea varios primitivos a la vez. Si se intenta unir & dos tipos primitivos se obtendrá el tipo nunca, representado por la palabra clave never:

type NotPossible = number & string;
// Type: never

La palabra clave y el tipo never es lo que los lenguajes de programación denominan como tipo inferior, o tipo vacío. Un tipo inferior es aquel que no puede tener valores posibles y no puede ser alcanzado. No se pueden proporcionar tipos a una ubicación cuyo tipo sea un tipo inferior:

let notNumber: NotPossible = 0;
//  ~~~~~~~~~
// Error: Type 'number' is not assignable to type 'never'.

let notString: never = "";
//  ~~~~~~~~~
// Error: Type 'string' is not assignable to type 'never'.

La mayoría de los proyectos TypeScript rara vez -o nunca- utilizan el tipo never. Aparece de vez en cuando para representar estados imposibles en el código. La mayoría de las veces, sin embargo, es probable que se trate de un error por un mal uso de los tipos de intersección. Lo trataré más en el Capítulo 15, "Operaciones de tipos".

Resumen

En este capítulo, has ampliado tus conocimientos sobre el sistema de tipos de TypeScript para poder trabajar con objetos:

  • Cómo TypeScript interpreta los tipos a partir de literales de tipo de objeto

  • Describir los tipos literales de objeto, incluidas las propiedades anidadas y opcionales

  • Declarar, inferir y estrechar tipos con uniones de tipos literales de objetos

  • Uniones discriminadas y discriminantes

  • Combinar tipos de objeto con tipos de intersección

Consejo

Ahora que has terminado de leer este capítulo, practica lo que has aprendido en https://learningtypescript.com/objects.

¿Cómo declara un abogado su tipo TypeScript?

"¡Protesto!"

Get Aprender TypeScript now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.