Video Summary and Transcription
Esta charla explora el concepto de tipos y su relación con las variables en TypeScript, incluyendo tipos primitivos, tipos especiales y tipos literales. También profundiza en las uniones e intersecciones de tipos, su forma canónica y su efecto en conjuntos de valores. La charla discute los tipos de objetos, sus miembros definidos y el comportamiento de las comprobaciones de acceso a propiedades. Destaca cómo se pueden utilizar las uniones e intersecciones con objetos y cómo se reducen a una forma canónica. También se enfatiza la importancia de los tipos base en TypeScript y cómo permiten que las variables contengan instancias de cualquier subtipo.
1. Introduction to Types and Values
Hola a todos. Mi nombre es Tizian Cernikva Dragomir. Trabajo en el equipo de infraestructura de JavaScript en Bloomberg y contribuyo al compilador de TypeScript. En esta charla, exploraremos el concepto de tipos y cómo se relacionan con las variables. Los tipos pueden verse como una representación y comportamiento de los datos, pero también pueden verse como un conjunto de valores que una variable puede poseer. Discutiremos los tipos primitivos en TypeScript, como number, string, Boolean, así como los tipos especiales como never y unknown. Además, exploraremos los tipos literales y su aplicación en operaciones de conjunto, como la unión.
♪♪ Hola a todos. Mi nombre es Tizian Cernikva Dragomir. Bienvenidos a mi charla, TypeSets Sets. Un poco sobre mí primero. Trabajo en el equipo de infraestructura de JavaScript en Bloomberg. Contribuyo al compilador de TypeScript. Y si han oído hablar de mí, probablemente me hayan conocido en Stack Overflow, donde respondo muchas preguntas sobre TypeScript.
Así que quiero comenzar esta charla con una pregunta muy simple. ¿Qué es un tipo? A menudo, son preguntas muy simples las que nos permiten obtener nuevas ideas. Cuando comenzamos a aprender a programar, veremos los tipos como algo que asociamos con una variable. Probablemente también aprendamos que este tipo de datos tiene algo que ver con la forma en que los datos se representan en la memoria. Por ejemplo, los enteros se representan en 32 bits, las cadenas son una secuencia de caracteres, los objetos también se representan en la memoria de una manera particular. Así que nos quedamos con la impresión de que un tipo es representación y comportamiento, comportamiento que significa los operadores que pueden manipular esos valores. Pero también hay una forma diferente de ver un tipo, a saber, como un espacio de valores.
Entonces, un tipo es un conjunto de valores que la variable puede poseer. Veamos algunos de los tipos primitivos en TypeScript y cómo se relacionan con esta definición. Por ejemplo, el tipo number es el conjunto de todos los valores de punto flotante. El tipo string es el conjunto de todos los valores de texto. El tipo Boolean es el conjunto de valores true y false. También hay algunos tipos especiales en TypeScript, a saber, el tipo never, que, como no tiene valores posibles que puedan existir en tiempo de ejecución, representa el conjunto vacío. Y el tipo unknown, que representa el conjunto de todos los valores posibles en JavaScript. Así que tomemos esta nueva forma de ver los tipos y tratemos de aplicarlos a algunos tipos que probablemente ya conozcamos. Intentemos crear un tipo que describa un conjunto con un único valor. En TypeScript, estos tipos se denotan por sus valores asociados. Entonces podemos definir un tipo que sea el valor Sí, 1 o Verdadero. Y una vez que hayamos definido estos tipos, si los asociamos con una variable, entonces esa variable solo puede contener el valor que forma parte de ese conjunto. Por ejemplo, si asociamos Sí con una variable, solo puede contener Sí. Nunca podrá contener el valor No o cualquier otro valor de cadena. Ahora, los tipos literales no son particularmente útiles hasta que los asociamos con una de las operaciones básicas que podemos hacer en conjuntos, a saber, la unión. Cuando creamos una unión de dos conjuntos existentes, creamos un nuevo conjunto que contiene los valores de ambos conjuntos originales.
2. Union and Intersection of Types
En TypeScript, crear una unión a partir de varios tipos permite crear un nuevo tipo que puede tener valores de todos los tipos constituyentes. La intersección de los tipos string y number da como resultado el conjunto vacío representado por el tipo never. Al intersectar un tipo similar a boolean con el conjunto de strings se obtienen los valores Sí y No. TypeScript reduce los tipos que contienen operadores de unión e intersección a una forma canónica, expresándolos como una unión de intersecciones.
Entonces, de manera similar, en TypeScript, si creamos una unión a partir de nuestros tres tipos existentes, utilizando el operador pipe, lo que sucederá es que crearemos un nuevo tipo que puede tener valores de los tres tipos constituyentes. Por lo tanto, nuestro nuevo tipo puede contener los valores 1, True y Yes, pero no puede contener, por ejemplo, el valor No.
Echemos un vistazo a la otra operación que podemos realizar en conjuntos, a saber, la intersección. Supongamos que tenemos dos tipos primitivos, a saber, string y number, y queremos ver cuál sería la intersección de estos dos tipos. Bueno, ¿qué valor en tiempo de ejecución es tanto un string como un número? La respuesta es que no hay tal valor. Estos dos conjuntos están completamente desconectados. Por lo tanto, su intersección sería el conjunto vacío. Y TypeScript representa el conjunto vacío a través del tipo never. Por lo tanto, esta intersección sería Never.
Pero veamos si podemos hacer algo más útil con las intersecciones. Supongamos que tenemos esta unión de tipos literales. Y nos gustaría extraer los componentes de string de esta unión. ¿Cómo podríamos usar las intersecciones en este caso? Bueno, si intersectamos un tipo similar a boolean con el conjunto de strings, ¿qué valores estarían en esta intersección? Y la respuesta es solo los valores Sí y No. Los otros valores no cumplirían con los criterios, ¿verdad? 0 y 1 no son tipos de string, tampoco lo son True y False. Entonces, si creamos este tipo, TypeScript estará de acuerdo con nosotros y dirá que solo los strings son Sí y No. Y de manera similar, podríamos extraer los números si intersectamos con number. También podríamos extraer los booleanos si intersectamos con boolean.
Ahora algo muy interesante sucede aquí, ya que boolean solo contiene True y False. Dado que el resultado de esta intersección sería solo True y False, lo que boolean termina siendo es, por supuesto, solo el tipo boolean porque boolean es solo la unión de los literales True y False. Pero veamos por qué exactamente esto funciona. ¿Por qué TypeScript realiza esta reducción? Bueno, TypeScript intentará llevar todos los tipos que contienen operadores de unión e intersección a una forma canónica. Es decir, intentará expresar nuestro tipo como una unión de intersecciones y aplicará la distributividad para llegar a este resultado. Por lo tanto, por ejemplo, si tomamos el tipo de solo strings que creamos antes, lo que TypeScript hará primero es expandir esa unión. Y luego intentará llevar la intersección y acercarla a cada uno de los constituyentes de la unión. Así que obtendremos este tipo. ¿Qué sucede ahora? Bueno, si tomamos la intersección de String, que contiene todos los valores de string, y del tipo Yes, que es el conjunto que contiene solo el valor Yes. ¿Qué obtenemos? Bueno, la respuesta es que obtenemos solo el valor Yes. Obtenemos Yes de esto. Por lo tanto, no tenemos que mantener todo esto, podemos dejar solo el tipo Yes. ¿Y qué sucede con No? Bueno, la situación es similar.
3. Intersection of Types and Object Types
No y String resultará en No. La intersección de Zero y String es Never. Lo mismo ocurre con 1, True y False. Después de reducir las intersecciones, nos queda Yes o No. Los tipos de objeto definen un conjunto de valores de objeto con miembros definidos. Pueden estar presentes otros miembros. Excepción: las comprobaciones de acceso a propiedades detectan errores al asignar literales de objeto con más propiedades. object.keys devuelve una matriz de strings, no se puede indexar de nuevo en el objeto. Este comportamiento es intencional.
No y String simplemente resultará en No. ¿Qué pasa con la intersección de Zero y String? Bueno, el valor Zero, que está en el conjunto denotado por el tipo literal, no tiene nada en común con el tipo string. Por lo tanto, el resultado es la unión vacía, que es, por supuesto, Never. Y lo mismo ocurre con 1, True y False. Todos estos resultarán en tipos Never. Entonces, esto es lo que nos queda después de esta reducción de intersecciones.
¿Qué sucede ahora? Bueno, si hacemos una unión con el conjunto vacío, el conjunto vacío no agregará nada a nuestro conjunto. Es lo mismo que si no hubiéramos realizado esa operación. Por lo tanto, podemos eliminar todas esas uniones con Never y nos quedará Yes o No.
Bien, veamos los tipos de objeto a continuación. ¿Qué define un tipo de objeto? Bueno, define un conjunto de valores de objeto que deben tener los miembros definidos por el tipo de objeto. Ahora, crucialmente, lo que un tipo de objeto no hace es evitar que haya otros miembros presentes en el objeto. ¿Qué quiero decir con esto? Veamos un ejemplo. Veamos un tipo de persona que tiene una propiedad de nombre y tengamos una función que toma un parámetro de este tipo. ¿Qué podemos pasar? Bueno, podemos pasar un objeto que tenga menos propiedades. La propiedad de nombre es requerida por el tipo de persona. Obviamente, podemos pasar un objeto que tenga la propiedad de nombre. Eso está bien. Pero también podemos pasar un objeto que tenga más que eso, que tenga la propiedad de nombre y la propiedad de edad. Ahora TypeScript está perfectamente feliz de aceptar esto.
Ahora, hay una excepción a este comportamiento que se llama comprobaciones de acceso a propiedades, y esto ocurre cuando realmente obtienes un error si intentas asignar un literal de objeto con más propiedades a un parámetro o una variable que tiene un tipo específico. Este comportamiento solo se activa si el literal de objeto es nuevo, es decir, si lo estás creando allí mismo, y es parte del pragmatismo de TypeScript. Es algo que TypeScript hace, aunque no sea necesariamente consistente con el resto del sistema de tipos, pero atrapa una cierta clase de errores que dificultarían la vida de los desarrolladores si no los atrapara.
Entonces, dentro del conjunto definido por este tipo de objeto, podríamos tener objetos que solo tienen la propiedad de nombre, pero también tenemos objetos que tienen la propiedad de nombre y pueden tener cualquier número de otras propiedades. Ahora, una consecuencia interesante de esto es el comportamiento de object.keys, que seguramente sorprenderá a muchos desarrolladores. object.keys devolverá una matriz de strings, lo que significa que en realidad no podemos usar el resultado de object.keys para indexar de nuevo en el objeto del que obtuvimos las claves. Por lo tanto, p de clave aquí dará un error. Seguramente este es un comportamiento muy frustrante, es muy frustrante para mí, y lo que la gente suele hacer es agregar una afirmación de tipo y decir as clave de lo que sea que le pasamos. Y probablemente mucha gente piense, bueno, TypeScript sigue siendo un lenguaje nuevo, tal vez esto sea un descuido, simplemente no han tenido tiempo de solucionarlo. Sé que eso es lo que pensé al principio, y sé que mucha gente envía solicitudes de extracción para tratar de solucionar esto, pero esto en realidad no es un descuido, esto es intencional.
4. Objects, Unions, and Intersections
Podemos pasar objetos con más claves de las esperadas, lo que puede provocar errores en tiempo de ejecución. Las uniones de objetos crean un conjunto de valores con propiedades que pueden o no estar presentes. Las intersecciones de tipos de objetos resultan en valores de objeto con propiedades de ambos tipos. Las uniones e intersecciones se nombran en función de su efecto en los conjuntos de valores, no en función de su efecto en los miembros. Se puede lograr filtrar una unión discriminada utilizando intersecciones.
Y la razón es que en realidad podemos pasar objetos que tienen más claves que las de una persona. ¿Qué sucede si pasamos el objeto P1, que tiene una propiedad de nombre y una propiedad de edad? Bueno, el bucle for tomará la primera clave, que es nombre, la convertirá en mayúscula y todo funcionará bien, pero luego tendrá la clave de edad y cuando indexe con la clave de edad obtendrá un número, que no tiene el método toUpperCase, y esto fallará en tiempo de ejecución. Entonces, si object.keys devolviera una matriz de las claves que pasamos como tipo, podríamos obtener en tiempo de ejecución más claves de las que esperábamos, y esto podría provocar errores en tiempo de ejecución. Y esta es la razón por la que object.keys tiene el tipo que tiene, porque si no lo tuviera, si devolviera una matriz de clave de lo que pasamos, podríamos obtener errores en tiempo de ejecución que no esperábamos y errores en tiempo de ejecución en un programa que verifica completamente los tipos.
Pasemos ahora a las uniones de objetos. ¿Qué significan? Bueno, tomemos un ejemplo. Digamos que tenemos dos tipos de objetos y queremos tomar su unión. Bueno, esto significa que nuestro nuevo conjunto, nuestra unión, tendrá valores de objeto que pueden tener una propiedad de nombre o una propiedad de ID, pero no sabemos exactamente cuál. Por lo tanto, no es seguro acceder a ninguna de estas propiedades porque es posible que no estén presentes. Una de ellas lo estará, pero no sabemos cuál. Si estos tipos tuvieran alguna propiedad en común, por ejemplo, descripción, entonces esa propiedad sería segura de acceder porque podemos decir con certeza que debe estar presente en todos los valores de objeto dentro de este conjunto. Y esto nuevamente es una fuente de confusión para muchos principiantes en TypeScript porque la operación de unión se nombra en función de lo que hace en el conjunto de valores, no necesariamente en función de lo que hace en los miembros porque las uniones permitirán el acceso a una intersección de miembros. Pero nuevamente, la operación se nombra en función de lo que hace en los conjuntos de valores descritos por estos tipos.
¿Qué hay de las intersecciones de tipos de objetos? Bueno, sufren una confusión similar. Es decir, si tomamos la intersección de estos dos conjuntos, lo que tendremos en la intersección son valores de objeto que tienen tanto el nombre como el ID. Y dado que tienen ambas propiedades, es seguro acceder a cualquiera de ellas. Entonces, la intersección de tipos de objetos intersectará los conjuntos definidos por los tipos y dará como resultado un nuevo tipo que tiene una unión de miembros. Pero nuevamente, la operación se nombra en función de lo que hace en los conjuntos, no en función de lo que hace en los miembros. Entonces, nuevamente, las uniones e intersecciones, muchas personas tienen la sensación al principio de que estos nombres son incorrectos porque, bueno, en las uniones tenemos una intersección de miembros, mientras que en las intersecciones tenemos una unión de miembros. Pero nuevamente, no se nombran en función de lo que hacen en los miembros, se nombran en función de lo que hacen en los conjuntos de valores.
Veamos si podemos usar el mismo truco que usamos en las uniones de tipos primitivos para filtrar una unión discriminada. Y aquí tenemos una unión discriminada que tiene dos componentes, uno de tipo cuadrado y otro de tipo círculo. ¿Cómo podemos extraer solo el componente de círculo? Y, por supuesto, podríamos usar un tipo condicional, que es definitivamente la forma preferida de hacer esto, pero las intersecciones también pueden hacer el trabajo. Y en su mayoría dan los mismos resultados. Entonces, ¿qué tipo podríamos intersectar con esta unión para preservar solo el tipo que tiene el tipo círculo? Y la respuesta es que podemos intersectar con otro tipo de objeto que tiene una propiedad de tipo círculo. Y esto realmente preservará solo el componente en el que estamos interesados porque la intersección no contiene ninguno de los objetos que podrían tener el tipo cuadrado. Entonces veamos cómo podría funcionar esto. Bueno, TypeScript intentará hacer lo mismo que hace para la unión de tipos primitivos, es decir, intentará llevarlo a la forma canónica. Entonces, en primer lugar, TypeScript expandirá esa forma en la unión que realmente es.
5. Intersection and Union Types in TypeScript
TypeScript reduce los tipos de objetos con intersecciones y uniones a una forma canónica. La intersección de los tipos círculo y cuadrado es el tipo never, lo que resulta en un tipo de objeto con una propiedad de tipo never. Esto ilustra cómo se pueden usar las intersecciones en los tipos de objetos.
Y acercará la intersección a cada uno de los componentes de la unión. ¿Qué sucede ahora? Bueno, TypeScript notará que el segundo componente de esta unión tiene una intersección que tiene el tipo círculo y el tipo cuadrado. Por lo tanto, este tipo de propiedad tendrá que ser círculo y cuadrado al mismo tiempo. Ahora, la intersección de estos dos tipos es en realidad el tipo never. No hay ningún valor posible que pueda ser al mismo tiempo el círculo constante y el cuadrado constante. Por lo tanto, reducirá este tipo de objeto a algo que tiene una propiedad de tipo never. Luego notará que, bueno, esto en sí mismo es equivalente a never porque no hay ningún valor, ningún valor de objeto que pueda tener una propiedad de tipo never. Por lo tanto, esto también es never. Ahora, después de hacer esto, nuevamente nos queda una unión con never. Por lo tanto, esto simplemente se puede eliminar y nos queda esta intersección. Aunque este tipo funcionará de la misma manera que nuestro tipo de círculo, el que tiene el radio también, es diferente de lo que obtendríamos de un tipo condicional, del tipo condicional extraído. Pero nuevamente, hace el trabajo igual de bien. Y esto ilustra cómo funcionan las intersecciones y cómo pueden funcionar para los tipos de objetos.
6. Understanding Base Types
Un tipo base en TypeScript permite que una variable contenga instancias de cualquier subtipo. El tipo base describe el conjunto que contiene todos los valores de subtipo, y un subtipo siempre es un subconjunto dentro del tipo base.
Otra pregunta que quiero hacer es, ¿qué es un tipo base? Y nuevamente, esta es una de esas preguntas que puede ofrecer más información. Especialmente, ¿qué es un tipo base cuando hablamos de conjuntos? Y nuevamente, nuestra intuición está moldeada por los lenguajes de tipo nominal. Siempre podemos verificar la definición del tipo en el lenguaje de tipo nominal para una cláusula extends o una cláusula implements. Y esto generalmente nos da una buena idea, especialmente para las clases, de cuál es la clase base. Entonces, por ejemplo, en este caso, en C++, simplemente podemos verificar lo que viene después de los dos puntos, y vemos que dog extiende animal. Lo mismo en Java, podemos ver que dog extiende animal, por lo que animal es el tipo base de dog. Y lo mismo para C Sharp. Pero, ¿qué significa realmente tener una variable con un tipo base? ¿Qué puede contener esa variable en realidad? Bueno, puede contener una instancia de un animal, pero también puede contener una instancia de un perro, o una instancia de un gato, o una instancia de cualquier subtipo de animal. Entonces, en términos generales, si tienes una variable con un tipo base, puede contener instancias de cualquier subtipo. Por lo tanto, dentro de nuestro tipo animal, los valores posibles son instancias de animal, de perro, de gato, e incluso, dado que TypeScript es un lenguaje con tipado estructural, un sistema de tipos estructuralmente tipado, cualquier objeto compatible que se ajuste a la descripción de animal también puede estar presente dentro del conjunto. Entonces, ¿qué conclusiones podemos sacar de aquí? Bueno, un tipo base describe el conjunto que contiene todos los valores de subtipo, y un subtipo siempre es un subconjunto dentro del tipo base.
7. Encontrando Tipos Base en TypeScript
Para encontrar el tipo base de un tipo de objeto en TypeScript, podemos considerar los conjuntos de los que el tipo es un subconjunto. Un tipo puede tener múltiples tipos base y no es necesario que estén definidos explícitamente. El número de tipos base puede ser infinito, especialmente al considerar propiedades opcionales. TypeScript considera que un tipo con nombre y descripción es un tipo base para un tipo con edad y nombre, ya que incluye objetos con nombre y edad. Por lo tanto, podemos encontrar cualquier número de tipos base para un tipo.
Entonces, dado todo esto, ¿cómo podemos encontrar el tipo base de este tipo de objeto, por ejemplo? Bueno, la pregunta se convierte en ¿de qué conjuntos es este conjunto un subconjunto? Bueno, podríamos ver este conjunto como un subconjunto del conjunto descrito por un tipo de objeto que solo tiene la propiedad nombre, pero también podríamos verlo como un subconjunto del tipo que solo tiene la propiedad edad. Entonces, ¿qué conclusiones podemos sacar de esto? Bueno, un tipo puede tener múltiples tipos base en TypeScript, y no necesariamente necesitan ser especificados. Simplemente funcionan de esta manera, porque los conjuntos están incluidos uno en otro. Además, al considerar propiedades opcionales, el número de tipos base podría ser infinito, porque si preguntamos a TypeScript, ¿un tipo que solo tiene edad y nombre extiende un tipo que tiene nombre y descripción opcional, TypeScript dirá que sí. Y esto es bastante claro, porque el tipo con nombre y descripción también incluye cualquier objeto que tenga nombre y edad, porque nuevamente, es solo un subtipo de este tipo más grande. Por lo tanto, de hecho, podemos encontrar cualquier número de tipos base para nuestro tipo. Yendo más allá, otra fuente de tipos base podrían ser las uniones. ¿Qué quiero decir con esto? Bueno, si tomamos la unión de dos tipos aleatorios, los constituyentes siempre van a ser subconjuntos de la unión. Entonces, por definición, por ejemplo, T va a estar incluido en la unión de T y cualquier otro tipo que estemos agregando. Por lo tanto, T siempre va a ser un subtipo de cualquier unión en la que esté presente. Entonces, nuevamente, el número de tipos base simplemente incluye otra cantidad infinita de posibles tipos. Entonces, dado nuestro nuevo entendimiento de lo que es el tipo base y lo que es el subtipo, veamos un ejemplo donde nuestra intuición podría fallarnos al principio. Aquí tenemos una función donde el parámetro debe ser un tipo de objeto donde las claves extienden la unión de top y bottom. Bueno, ¿qué tipo de objeto podemos pasar aquí? Bueno, podríamos pasar un objeto que tenga la propiedad top. Podríamos pasar un objeto que tenga tanto top como bottom. Pero no podríamos, por ejemplo, pasar un objeto que tenga top, bottom y left. ¿Por qué no? Bueno, porque left no es un subtipo de esta unión. Entonces, la inferencia fallará en este caso. Entonces, dado que T debe ser un subtipo de top y bottom, en efecto, solo puede tener estos dos valores. Solo puede ser top, bottom o una unión de estos dos. Ahora, no es que TypeScript no pueda inferir la clave left aquí si usamos T extends string. Bueno, entonces left es un posible subtipo de string. Entonces T se inferirá como top, bottom o left. Pero veamos un ejemplo donde pongamos la restricción de una manera diferente. Digamos que ahora tenemos T, T es un tipo de objeto, con propiedades top y bottom. Bueno, ahora un tipo que tiene top, bottom y left va a ser un subtipo de este tipo que tiene las propiedades top y bottom. Entonces, nuevamente, nuestra intuición podría estar un poco equivocada aquí porque generalmente esperamos que cuando decimos extiende, haya más. El subtipo tiene que tener más cosas. Pero eso no siempre es el caso. Tenemos que pensar en cómo funciona esto en términos de conjuntos en lugar de simplemente seguir nuestra intuición y pensar que tiene que haber más. Entonces, algunas conclusiones. Es posible que tengamos que desaprender algunas cosas. El sistema de tipos de TypeScript es consistente en sí mismo por lo general. Pero algunas de las relaciones de tipos pueden no parecer intuitivas al principio, pueden parecer extrañas. Pero en realidad tienen sentido cuando las vemos a través del prisma de los conjuntos. Muchas gracias y espero cualquier pregunta. Microsoft Mechanics www.microsoft.com.au
Comments