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:
o se les puede nombrar usando una interfaz:
o un alias de tipo:
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.
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
.
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.
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.
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 ↗
En un patrón de desestructuración de objetos,
shape: Shape
significa “tomar la propiedadshape
y redefinirla localmente como una variable llamadaShape
. Del mismo modoxPos: number
crea una variable llamadanumber
cuyo valor se basa en el parámetroxPos
.
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.
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í.
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.
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:
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:
Sin embargo, las propiedades de diferentes tipos son aceptables si la firma del índice es una unión de los tipos de propiedad:
Finalmente, puedes hacer que las firmas de índice sean readonly
para evitar la asignación a sus índices:
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.
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:
Sortear estos controles es realmente sencillo. El método más sencillo es simplemente utilizar una aserción de tipo:
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í:
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:
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:
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.
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
.
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
.
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.
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 &
.
Aquí, hemos cruzado Colorful
y Circle
para producir un nuevo tipo que tiene todos los miembros de Colorful
y Circle
.
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: string
s, number
s, Jirafa
s, lo que sea.
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.
Un enfoque seguro sería, en su lugar, crear diferentes tipos de Box
para cada tipo de contents
.
Pero eso significa que tendremos que crear diferentes funciones, o sobrecargas de funciones, para operar en estos tipos.
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.
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
.
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.
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).
Esto también significa que podemos evitar las sobrecargas por completo usando funciones genéricas.
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:
usando un alias de tipo en su lugar:
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.
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>
.
Al igual que el tipo Box
anterior, Array
en sí es un tipo genérico.
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.
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 ReadonlyArray
s, nos dice que no debemos cambiar el contenido en absoluto, y cuando vemos una función que consume ReadonlyArray
s, 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.
En su lugar, podemos asignar Array
s normales a ReadonlyArray
s.
Así como TypeScript proporciona una sintaxis abreviada para Array<Type>
con Type[]
, también proporciona una sintaxis abreviada para ReadonlyArray<Type>
con readonly Type[]
.
Una última cosa a tener en cuenta es que, a diferencia del modificador de propiedad readonly
, la asignabilidad no es bidireccional entre Array
s y ReadonlyArray
s normales.
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.
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
.
Si intentamos indexar más allá del número de elementos, obtendremos un error.
También podemos deestructurar tuplas ↗ usando la desestructuración de arrays de JavaScript.
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
y1
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 Array
s que declaran propiedades para índices específicos y que declaran length
con un tipo literal numérico.
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
.
Las tuplas también pueden tener elementos rest, que tienen que ser de tipo array/tupla.
StringNumberBooleans
describe una tupla cuyos dos primeros elementos sonstring
ynumber
respectivamente, pero que puede tener cualquier número deboolean
s siguientes.StringBooleansNumber
describe una tupla cuyo primer elemento esstring
y luego cualquier número deboolean
s y termina con unnumber
.BooleansStringNumber
describe una tupla cuyos elementos iniciales son cualquier número deboolean
s y terminan con unstring
y luego unnumber
.
Una tupla con un elemento rest no tiene un length
establecido; solo tiene un conjunto de elementos conocidos en diferentes posiciones.
¿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:
básicamente equivale a:
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.
Como es de esperar, escribir en cualquier propiedad de una tupla readonly
no está permitido en TypeScript.
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
.
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.