Hamburger Icon
Las bases de TypeScript (parte 3): tuplas, enums, any, unknown y never

Las bases de TypeScript (parte 3): tuplas, enums, any, unknown y never

En este post vamos a ver algunos tipos nuevos que introduce TypeScript y que no existen en JavaScript. Si hemos trabajado con otros lenguajes de programación como C# o Java, nos van a sonar muchos de estos conceptos.

Esta es la tercera parte de una serie de artículos sobre TypeScript. Si vas más perdido que un daltónico resolviendo un cubo de Rubik, vuelve para atrás para ver al menos cómo tipar funciones en TypeScript.

Tuplas

En JavaScript no tenemos tuplas, que son un elemento típico en muchos otros lenguajes. TypeScript nos permite usar tuplas, que no dejan de ser arrays pero con una longitud fija.

Por ejemplo, en este objeto, la propiedad combustible la queremos tratar de forma que el primer elemento es siempre un identificador del tipo de combustible y el segundo elemento es el nombre:

const coche = {
  marca: "Peugeot",
  modelo: "207",
  combustible: [2, "diesel"],
}

Si hacemos esto, internamente se deduce que la propiedad combustible es un array que puede contener números y strings. Esto no nos serviría para definir una tupla, ya que podríamos añadir sin ningún tipo de problema un tercer elemento.

coche.combustible.push("otracosa") // no da ningún error

En este caso, queremos ser más precisos y sobreescribir la inferencia por defecto, que no está funcionando como queremos y por lo que sí definiremos el objeto de esta otra forma:

const coche: {
  marca: string
  modelo: string
  combustible: [number, string]
} = {
  marca: "Peugeot",
  modelo: "207",
  combustible: [2, "diesel"],
}

En este caso estamos indicando que combustible será una tupla con un primer elemento de tipo number y un segundo elemento de tipo string. Si ahora hacemos esto:

coche.combustible.push("otracosa") // tampoco da error

Tampoco obtendremos error, porque el método push es una excepción de la regla y TypeScript no va a poder detectar que nos estamos saltando este tipo. Funcionará de esta forma:

coche.combustible = "otracosa" // error
coche.combustible = ["gasoil", 1] // error
coche.combustible = [1, "gasoil", "otracosa"] // error
coche.combustible = [1, "gasoil"] // correcto

Por lo tanto, deberemos acordarnos de la excepción del método push, pero por lo demás, será buena idea usar tuplas cuando sepamos la longitud y el tipo de elementos que vamos a almacenar en cada posición de un array.

Enums

Al igual que pasa con las tuplas, este es otro tipo de datos muy común en otros lenguajes de programación que no existe en JavaScript. Con los enums podemos tener identificadores o constantes globales a las que podamos acceder de forma sencilla.

Es como una lista enumerada de cero a n en la que cada elemento se representa por una palabra concreta.

enum Rol {
  ADMIN,
  USUARIO,
  INVITADO,
}

ADMIN recibe el número 0 como valor, USUARIO el número 1 y INVITADO el número 2, pero ahora podemos usar las palabras que representan el rol para no tener que acordarnos de un número concreto.

enum Rol {
  ADMIN,
  USUARIO,
  INVITADO,
}

const usuario = {
  nombre: "Zeppelin",
  rol: Rol.ADMIN,
}

Si por lo que sea tenemos identificadores diferentes a 0, 1 y 2, podemos asignar el valor que nos convenga, sean numéricos o no:

enum Rol {
  ADMIN = 100,
  USUARIO = 110,
  INVITADO = "GUEST",
}

Los enums serán de mucha utilidad para estos casos donde tenemos que usar constantes para almacenar algunos valores y luego es necesario hacer comprobaciones varias, ya que las funcionalidades de autocompletado nos ayudarán a ahorrar tiempo y evitar errores:

if (usuario.rol === Rol.ADMIN) {
  // ...
}

Tipo any

El tipo any es propio de TypeScript y sirve para cuando no sabes de qué tipo será la variable a declarar o quieres usar cualquier tipo. Para evitar errores en la comprobación de tipos, podemos usar any, aunque abusar de any es una muy mala práctica.

const unaVariable: any = "Hola mundo"

// Esto provoca un error al interpretar el código
// pero no genera error en la comprobación de tipos
unaVariable()

// Esto sí que provoca un error de TypeScript
// porque un string no se puede llamar como una función
const unTexto: string = "Hola mundo"
unTexto()

Con el tipo any perdemos las ventajas de usar TypeScript.

Para evitar malas prácticas, podemos usar la opción noImplicitAny del compilador para que marcar con error todas las instancias de este tipo que encuentre.

Tipo Unknown

Seguramente no lo vamos a usar muy a menudo, pero puede ser útil en algunos casos, como cuando esperamos el input de un usuario. El tipo unknown es introducido por TypeScript y sirve para indicar que todavía no sabemos de qué tipo es una variable que definamos:

let inputUsuario: unknown

Posteriormente podemos guardar cualquier tipo de valor en esa variable, sin errores de compilación. Es como una versión más estricta que el tipo any. Mira este ejemplo:

let inputUsuario: unknown
let nombreUsuario: string

inputUsuario = "Zeppelin"
nombreUsuario = inputUsuario // error

El error se produce porque aunque en estos momentos la variable de tipo unknown sea una cadena, TypeScript no puede garantizar que siempre vaya a ser así. En estos casos, tenemos que asegurarnos de lo que estamos haciendo para que TypeScript no lance errores de compilación:

let inputUsuario: unknown
let nombreUsuario: string

inputUsuario = "Zeppelin"

if (typeof inputUsuario === "string") {
  nombreUsuario = inputUsuario // correcto
}

Si no sabemos de qué tipo va a ser una variable, siempre es mejor usar unknown antes que any. Aunque no sepamos de qué tipo va a ser, muy probablemente sabemos qué vamos a hacer con ella según el tipo que se acabe infiriendo y podemos usar estas comprobaciones extra de tipado para evitar errores.

Tipo Never

En algunas ocasiones podemos tener funciones que no devuelvan ningún valor. Ni siquiera void. Vamos a ver este ejemplo:

function imprimeTexto(mensaje: string): void {
  console.log(mensaje)
}

function lanzaError(mensaje: string, codigo: number): void {
  throw { message: mensaje, erroCode: codigo }
}

const retorno1 = imprimeTexto("Hola mundo")
console.log(`resultado imprimeTexto: ${retorno1}`) // undefined

const retorno2 = lanzaError("Error desconocido en el servidor", 500)
console.log(`resultado lanzaError: ${retorno2}`) // no se ejecuta

Mientras que la función imprimeTexto devuelve de forma implícita undefined, la función lanzaError no devuelve nada y hemos indicado void como tipo de retorno en ambos casos. Esto, aunque sea correcto, podemos mejorarlo.

En este caso sabemos que el parámetro throw va a interrumpir el hilo de ejecución y nunca se va a devolver ningún valor, ni siquiera undefined. Podemos usar el tipo never en estos casos:

function lanzaError(mensaje: string, codigo: number): never {
  throw { message: mensaje, erroCode: codigo }
}

Si no indicamos ningún tipo de retorno TypeScript inferirá el tipo void de forma automática. Esto es debido a que el tipo never es una inserción posterior y sirve para añadir más claridad a funciones de este tipo que sabemos que no van a devolver nada, ni siquiera undefined.

Hasta aquí la tercera parte de las bases de TypeScript. Si no has tenido suficiente, todavía hay más tipos nuevos que te pueden interesar, los verás en la última parte de esta serie, donde comentamos las uniones, literales, alias e interfaces. ¡No te lo pierdas!