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.
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.
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.
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 ↗
Cannot find name 'xPos'.2304Cannot find name 'xPos'.}
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.
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í.
// Pero no podemos escribir en la propiedad 'resident' misma de 'Home'.
home.resident= {
name: "Victor the Evictor",
age: 42,
};
}
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.
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:
let myArray:ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] ="Mallory";
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.
let mySquare = createSquare({ colour: "red", width: 100 });
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:
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í:
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:
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:
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.
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.
interface ColorfulCircle extendsColorful, 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 &.
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.
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.
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éricoBox 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).
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.
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>.
* 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.
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.
Así como TypeScript proporciona una sintaxis abreviada para Array<Type> con Type[], también proporciona una sintaxis abreviada para ReadonlyArray<Type> con readonly Type[] .
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.
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.
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.
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.
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.
¿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:
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.
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.
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.