Como ya hemos visto anteriormente, TypeScript ofrece la posibilidad de trabajar con tipos de datos estáticos. Lo que significa que, una vez definido el dato, no se puede cambiar/modificar el tipo de dato.

La transpilación a JS siempre será a sin tipado de dato debido a que JS es de tipado débil y dinámico. Por tanto, trabajamos en TS y posteriormente traducimos a JS

¿Qué es un tipo de dato?

Un tipo de dato es un atributo que será nuestro mecanismo para indicarle a TypeScript, el tipo de dato que almacenará en una variable, constante, etc.

Beneficios de definir un tipo de dato

Aunque no son obligatorios en TypeScript, sí que son muy aconsejables, ya que nos permitirán realizar escritura segura tanto en TS como en JS (una vez realizada la transpilación).

De hecho, y como ya vimos, trabajar con tipos de datos estáticos nos va a permitir verificar en tiempo real (en tiempo de desarrollo) si el dato introducido eso correcto o no. Ya que el compilador analiza los tipos y en caso de detectar un error, nos lo mostrará dicho error por pantalla.

Para definir un tipo de dato en JavaScript tenemos alternativas a utilizar:

  •  Inferencia de tipos: será TypeScript el encargado de asignar un tipo de dato a una variable dependiendo del valor inicial que le asignemos a dicha variable.
      • Inferencia de tipos (sin definir un tipo de dato): establecerá el tipo de dato como any (cualquier tipo de dato será válido).

Si situamos el cursor sobre el tipo de dato podremos ver que es de tipo any:

      • Inferencia de tipos con valor: establecerá el tipo de dato (haciendo un typeof para obtener el tipo) sobre el valor que asignemos a dicha variable.

Si situamos el cursor sobre el tipo de dato podremos ver que es de tipo number:

  • Anotación de tipo: somos nosotros los que especificamos a TypeScript el tipo de dato con el que vamos a trabajar sobre una variable. Un ejemplo podría ser:

Si situamos el cursor sobre el tipo de dato, podremos ver que es un tipo de dato number:

Tipos de datos en TypeScript

Bien, una vez vistas las distintas maneras que tenemos/tiene TypeScript de definir un tipo de dato, vamos a ver qué tipos de datos disponemos en TS:

Tipos de datos primitivos en TypeScript

Como ya vimos en el artículo de Tipos de datos primitivos en JavaScript, los tipos de datos primitos en TypeScript son:

  • number: 1, -6.1, 0, 2.3, -5…

  • bigint: es un tipo de dato sacado en las últimas versiones de ES, compatible solamente con versiones a partir de la versión ES2020. Por lo que al transpilar nuestro código de TS, no podremos trabajar con este tipo de dato si queremos hacerlo compatible con versión más antigua que ES2002, como podría ser por ejemplo ES5. Por lo que se desaconseja su uso por el momento.

  • string: almacena cadenas de caracteres (entre los que se incluyen también números). Ejemplos: «hola», «mundo», etc.

  • boolean: almacena true o false (verdadero o falso).

  • symbol: nuevo en ECMAScript 6 por lo no es compatible con versiones inferiores a ES6. Nos permite crear objetos únicos.

  • undefined: en JS normal, cuando una variable no tiene un valor establecido, se establece undefined como valor (es decir, no definido). En cambio, en TS, aunque también se establece undefined como valor hay que recordar que este tipo de dato no podrá cambiar y, por tanto, no podrá ser diferente a undefined. Se suele trabajar con undefined principalmente cuando declaramos que una variable trabajará con el tipo de dato any (lo veremos un poco más abajo) ya que si no, nunca podrá cambiara otro valor que no sea undefined. O cuando queremos trabajar con un String sin darle valor, etc.

  • null: vacío (definido como vacío). Pasa lo mismo que con undefined, no puede cambiar a otro valor que no sea null. Por tanto, se suele utilizar cuando hemos declarado un tipo de dato como any ya que sino no tiene sentido ya que nunca podrá cambiar.

No primitivos

Existen muchos datos no primitivos:

  • any (alguno): el tipo de dato comodín, puede contener cualquier tipo de dato. Lo usamos principalmente en situaciones donde el tipo de dato puede cambiar o nosotros tengamos intención de reutilizar esa variable con otro tipo de dato.

Utilizar any constantemente no es muy recomendable ya que se va a perder el propósito el sentido por el cual fue creado Typescript. Así que, es mejor utilizarlo solamente cuando se nos presente una casuística en la que sea totalmente necesario su uso. Un ejemplo de un buen uso, puede ser un array con diferentes tipos de datos.

  • object: si miramos la imagen, es el tipo de dato que engloba a la mayoría de los no primitivos.

Si yo quiero controlar el tipo de dato de los atributos (también conocidos como propiedades) de un objeto ya que por ejemplo lo quiero declarar con el valor sin definir por el momento. Pero que en un futuro sea un String:

Puedo realizar:

Y si chequeamos nuevamente, podemos ver que ahora si que lo hemos realizado correctamente:

También podemos anidar objetos dentro de objetos de la siguiente manera:

  • array: estructura de datos que nos permite almacenar varios (una sucesión) elementos de un mismo tipo.

Existe la posibilidad de utilizar el tipo de dato any (comodín) si queremos recibir valores de distinto tipo para cada uno de los elementos del mismo:

  • tuple: es muy similar a un array con la diferencia que en un tuple podemos controlar/definir el tipo de dato que introducimos para cada uno de los elementos de dicho array. En el array todos son manzanas, el tuple, no tiene por qué.

Si enfrentamos a tuple contra un objeto podemos ver que tienen similitudes y diferencias que los hacen peculiares. Lo mejor es conocerlos en profundidad, con el fin de poder encantarnos entre uno u otro, dependiendo del análisis de los requisitos de la tarea que tenemos que hacer. Ya que, en según qué casuísticas tuple se adaptará mejor a algunas y en otras, el vencedor será el objeto.

Si intentamos modificar el valor del elemento 1 (los arrays siempre empiezan por 0) por un string, al tener dicha posición un tipo number, nos dará un error.

  • type: nos permite extraer/separar los tipos de datos asignados a la tuple, a objectos,etc. Con el fin de poderlos reutilizar sobre varios elementos sin que exista redundancia (duplicidad de lo mismo).

  • enum: los enums no existen en JS, por tanto, nos encontramos ante una de las pocas características que no es una extensión de tipo de dato de JavaScript. Los enums, nos permite definir una lista de constantes (elementos fijos) que no cambian y que podemos reutilizar sobre varias clases u objetos, etc a lo largo de nuestro proyecto.

Hay que destacar que nos devuelve un tipo numérico, no el valor defensa. En este caso en particular, nos devuelve 1. Si usamos delantero, devolverá 3, portero 0, etc.

Pero podemos recuperar el valor también pidiendo la posición de dicho valor de la siguiente manera:

No realizo la transpilación debido a que tengo un plugin instalado que me la hace por mí. Os dejo un artículo por si lo queréis instalar: Configurando la Transpilación (automática) de TypeScript mediante a un plugin

  • union: los tipos de datos unión son un tipo peculiar, ya que nos permiten trabajar con varios tipos de datos que nosotros especifiquemos sobre una variable. Vamos a ver un ejemplo:

Podemos hacer uso de type para almacenar el conjunto de datos como ya hicimos con los tuples anteriormente.

  • literal: El valor que yo asigno se toma como un tipo y ese tipo ya no lo puedo cambiar.

Así como tal poco uso le podemos dar. Pero si lo combinamos con unión, este tiene mucho potencial.

  • function: si realizamos una función que sume, podemos ver que el resultado que como recibe dos números por parámetros, el resultado que va a devolver TypeScript por defecto es un number. Ya que si no especificamos notación de tipos a una función TS lo realiza por inferencia analizando los valores y dictaminando cual será el tipo de return esperado.

Pero esto lo podemos editar y modificar a nuestro gusto. Por ejemplo, lo vamos a modificar a un string:

Si queremos controlar el tipo de dato que vamos a recibir en un function con el fin de no recibir parámetros de más. En este caso, por ejemplo, estamos controlando que solo recibamos una función, pero no los paremetros que vamos a recibir:

Si ahora modificamos a suma, podemos ver que también funciona (podemos hacer lo que hemos realizado con Function anteriormente para asegurarnos que se asigna una función inclusive):

Pero lo que queremos es verificar los parámetros . Por ello, utilizamos:

      • void: vacío, es ideal para una función sobre la queremos controlar que no devuelva nada. Por tanto, nos permite realizar una función sin return, ya que TS no espera nada.

Si intentamos hacer un return con Hola Mundo, podemos ver que nos muestra un error:

Void nos va a permitir realizar un return de undefined o de null:

      • never: en cambio, si le especificamos never, podemos ver que no podemos devolver ni un undefined ni un null incluidos. No podemos devolver absolutamente nada.

  • unknow: nos permite especificar que no conocemos el tipo de dato, es muy similar al any:

  • interface: nos permiten definir una estructura sobre la que podremos enfrentar/comparar si los datos de un objeto cumplen con dicha estructura o no. El principal beneficio de estás, es que se pueden reutilizar en varios objetos. Estableciendo una especie de requisitos que deberán cumplir los objetos sobre los que las implementaremos (utilizaremos).

Vamos a explicarlo detalladamente:

Si nosotros queremos imprimir los valores de un objeto sin realizar una interfaz, tendríamos que hacer algo así:

El problema del ejemplo anterior, es que cada vez que tengamos un objeto distinto, tendremos que crear una estructura como la que acabamos de crear y, por tanto, sería bastante redundante.

Si ahora definimos un entrenador, también tendremos que hacer lo mismo:

En cambio, las interfaces, nos permiten asegurar que los objetos que hemos creado cumplen los requisitos de la clase persona. Sin importar que sean futbolistas o entrenadores ya que ambos tienen la misma estructura (la misma que tiene la interfaz Persona).

Son muy parecidas a los tipos de dato type. De hecho, si substituimos interface por type, podemos ver que esto también funciona correctamente.

Aunque ambas, tienen ciertas peculiaridades. Las interfaces solamente nos permiten definir las estructuras de un objeto. Nos permite definir la estructura de un objeto antes de que el programador haya creado sus objetos. En cambio, type, nos permite utilizarse en objetos, y muchas más cosas. Por tanto, cuando definimos la estructura de un objeto solemos utilizar interface para que quede más clara nuestra intencionalidad sobre lo que estamos haciendo.

Por último, hay que destacar que las interfaces obligan a que existan unos tipos de datos, pero no exigen que no puedan existir más propiedades. Por tanto, los objetos deben no deben tener idénticamente la misma estructura que sus interfaces, pero como mínimo sí que deberán contener las mismas propiedades que su interfaz.

  • class: nos permite crear/definir objetos que tienen un conjunto de propiedades y métodos similares que con los que definiremos gran parte de las características de este conjunto de objetos.

Un ejemplo podría ser Persona, y dependiendo de muchos factores como la nacionalidad de la persona, el clima de dicha zona del mundo, una persona tendrá un color de piel, unos rasgos en los ojos, una manera u otro de ser, unas costumbres gastronómicas y culturales, etc.

Si creamos una persona, podemos ver que al imprimir su propiedad nacionalidad, nos aparece como vacía.

      • constructor: esto es debido a que tenemos que crear un constructor para poder trabajar con el valor de la propiedad class.

En este caso, cuando creamos la clase con new class, lo primero en ejecutarse será el constructor. En la zona de parámetros de dicho constructor, le pasamos el valor de la nacionalidad (nac, aunque podría llamarse también nacionalidad) que vamos a definir en nuestra propiedad de la clase. Y dentro de dicho constructor, asociamos el valor que recibimos al crear el objeto con el de la  nacionalidad que le definiremos entre los () de new Persona().

Con this, nos referimos al valor de la clase. Y el de sin this, es el parámetro que recibimos al crear el constructor

También podemos añadir métodos a nuestro objeto:

Si intentamos modificar nuestra nacionalidad, podemos ver que un Español, pasa a tener nacionalidad Brasileña:

Si no queremos que eso pase, tendremos que usar final sobre los atributos, con el fin de encapsularlos y que solo se puedan modificar desde el interior de la clase Persona:

Existe la posibilidad de definir las propiedades de un objeto directamente desde su constructor. El resultado es el mismo que definirlas fuera con la diferencia que tenemos un código más elegante:

Ya hemos controlado con private que nadie desde fuera de nuestra clase pueda modificar las propiedades de nuestro objeto. Si queremos además controlar que solamente podamos cambiar/modificar el valor de una propiedad desde dentro del constructor, usaremos readonly.

También podemos extender clases de otras clases:

Añadiendo Getters & Setters. También podemos trabajar con Getters & Setters. Get nos permitirá acceder a un valor de una propiedad y set modificar la propiedad siempre y que no sea readonly claro. Es una buena práctica tener las propiedades como private y tener Getters y Setters ya que, de esta manera, encapsulamos la información.

Las clases estáticas (static) nos permiten trabajar con métodos de un objeto sin tener que realizar una instancia (una creación de un objeto persona haciendo un new Person()) .

Vamos a ver un ejemplo:

Las clases abstractas on permiten realizar la creación de dicha clase (no se puede instanciar). De hecho, si lo intentamos vemos que nos aparece un error.

No podemos utilizar las clases abstractas como tal, ya que son como una capa que aplicaremos sobre nuestras clases con el fin de proporcionales un conjunto de propiedades y métodos. Pero, dicha clase, no puede instanciarse por sí sola, necesita a otra clase con el fin de inyectanr todos sus métodos y propiedades dentro de la clase que la llama.

Las clases abstractas, nos sirven para separar los elementos comunes de varias clases, de los propios de cada una de ellas.Con el fin de evitar tener clases con los mismos atributos en cada una y tenerlos todos en una sola, ya que así serán más fácil de editar y de mantener.

El concepto de abstract, también se puede utilizar en métodos de la misma forma que con las clases.

Podemos además trabajar con clases abstractas e interfaces a la vez. Tanto con una como con varias:

  • intersection: una intersection significa que combinamos dos interfaces con el fin de crear un objeto que sea una fusión de ambas. Vamos a ver un ejemplo:

Si eliminamos un campo requido en una de la interfaces, podemos ver que:

Espero que os haya gustado ¡Un saludo y hasta la próxima!