Type Guards & Type Assertions

devjosemanuel

DevJoseManuel

Posted on April 30, 2024

Type Guards & Type Assertions

En este artículo vamos a ver cómo podemos filtrar correctamente los elementos de un array quedándonos con los tipos que tienen asociados los elementos que lo conforman. Para ello vamos a suponer que partimos de un tipo de datos ResponseData que viene a simular la respuesta que nos ha proporcionado un API y dicho dato solamente tiene un atributo denominado data que es de tipo string:

type ResponseData = {
  data: string
}
Enter fullscreen mode Exit fullscreen mode

Ahora vamos a suponer que definimos un array de elementos que concuerdan con este tipado pero sin establecer que el array está formado por elementos de este tipo pudiendo de esta manera afirmar que está incluyendo objetos del tipo ResponseData:

const items = [{ data: 'Banana' }, { data: 'Dog' }]
Enter fullscreen mode Exit fullscreen mode

Para hacer las cosas más interesantes vamos a añadir algún elemento más al array que estamos asignando a items que sea undefined dejándolo definitivamente tal y como vemos a continuación:

const items = [{ data: 'Banana' }, undefined, { data: 'Dog' }, undefined]
Enter fullscreen mode Exit fullscreen mode

Ahora que tenemos nuestro conjunto de datos de partida lo que vamos a hacer es añadir la anotación del tipo que estará asociado con items que sabemos que será un array de que estará formado por objetos del tipo ResponseData o undefined, es decir, que lo definimos como sigue:

const items: (ResponseData | undefined)[] = [
  { data: 'Banana' },
  undefined,
  { data: 'Dog' },
  undefined
]
Enter fullscreen mode Exit fullscreen mode

Vamos ahora a definir una función que se encargue de filtrarnos todos los elementos del array que no son undefined por lo que escribiremos algo como lo siguiente:

const payloads = items.filter(items => item !== undefined)
Enter fullscreen mode Exit fullscreen mode

Y ya para terminar vamos a escribir en la consola del sistema todos aquellos elementos que forman parte del array payloads que, de forma intuitiva, podemos ver que se tratará de todos aquellos que no son undefined lo que nos dejará un código final que será similar al que se puede ver a continuación:

type ResponseData = {
  data: string
}

const items: (ResponseData | undefined)[] = [
  { data: 'Banana' },
  undefined,
  { data: 'Dog' },
  undefined
]

const payloads = items.filter(items => item !== undefined)

console.log(payloads)
Enter fullscreen mode Exit fullscreen mode

Vamos a guardar nuestro trabajo y a ejecutar nuestro código simplemente para comprobar que este se está comportando tal y como se espera que lo haga:

Problema

Como podemos observar todo ha funcionado tal y como esperábamos que lo hiciese pero si lo que hacemos es situar el cursor del ratón sobre la declaración del array payloads para que Visual Studio Code nos muestre cuál es el tipado que tiene asignado veremos que este seguirá siendo un array de objetos ResponseData o undefined como se puede ver en la siguiente imagen:

De forma ideal nos gustaría que a partir del filtrado que hemos realizado sea capaz de inferir cuál es el tipo que tendrá asignado a items sabiendo que este sería un array formado por elementos que son únicamente del tipo ResponseData.

Pero ¿cómo podemos lograr esto? Pues gracias a los Type Guards de TypeScript.

Type Guard

¿Pero en qué consiste crear un Type Guard? Pues en nuestro caso es que a partir de la función que tenemos asignada a la función que estamos pasando como argumento al método filter() tenemos que definir el tipo de datos que retornará y esto lo hacemos gracias al uso de la palabra reservada is de TypeScript como sigue:

const payloads = items.filter(
  (items): item is ResponseData => item !== undefined
)
Enter fullscreen mode Exit fullscreen mode

De esta manera que lo que estamos diciéndole a TypeScript es que le podemos asegurar que el resultado de la ejecución de dicha función es del tipo ResponseData de tal manera que si ahora ponemos el cursor sobre la definición de la variable payloads podemos ver que TypeScript nos indica que será un array de objetos de tipo ResponseData:

Es más, para ser mucho más explícitos podemos extraer el Type Guard a una función separada. Así pues vamos a crear una función a la que vamos a denominar isResponseData de tal manera que recibirá camo argumento un valor que puede ser de tipo RespondeData o undefined donde ademanas definiremos como tipo de retorno un Type Predicate como sigue:

function isResponseData(item: ResponseData | undefined): item is ResponseData {}
Enter fullscreen mode Exit fullscreen mode

Dentro de la función isResponseData lo que haremos será comprobar si el valor del parámetro es undefined o no de tal manera que si lo es retornaremos true y en caso contrario retornaremos false:

function isResponseData(item: ResponseData | undefined): item is ResponseData {
  return item !== undefined
}
Enter fullscreen mode Exit fullscreen mode

Con la declaración de nuestro Type Guard como una función ahora vamos a podérsela pasar como argumento al método filter() dejándonos el siguiente código completo donde mostramos todos los cambios que hemos realizado hasta este momento:

type ResponseData = {
  data: string
}

const items: (ResponseData | undefined)[] = [
  { data: 'Banana' },
  undefined,
  { data: 'Dog' },
  undefined
]

function isResponseData(item: ResponseData | undefined): item is ResponseData {
  return item !== undefined
}

const payloads = items.filter(isResponseData)

console.log(payloads)
Enter fullscreen mode Exit fullscreen mode

Si ahora guardamos nuestros cambios y volvemos a ejecutar nuestro código veremos que efectivamente nos volverá a mostrar por la consola un nuevo array de elementos que incluirán únicamente todos aquellos que no son undefined como se puede ver en la siguiente imagen:

y no solamente eso sino que además si ponemos el cursor dentro de Visual Studio Code sobre la variable payloads vemos que TypeScript es capaz de indicarnos que es del tipo ResponseData[] tal y como esperábamos:

Desventajas del uso de los Type Guards

Vamos a ver ahora las desventajas que están asociados al uso de los Type Guards tal y como hemos hecho hasta ahora y el principal error es que a la hora de definir el Type Predicate que está asociado a la función que ejerce de Type Guard es posible que la podamos definir con un tipo de datos que no le corresponda.

Por poner un ejemplo más claro a partir del código que hemos estado utilizando a lo largo de esta explicación, lo que tratamos de decir es que es posible declarar la función isResponseData como sigue:

function isResponseData(item: ResponseData | undefined): item is undefined {
  return item !== undefined
}
Enter fullscreen mode Exit fullscreen mode

y en este caso el tipo de datos que se inferirá para la variable payloads será un array de elementos undefined como podemos imaginarnos a partir de lo que acabamos de definir:

es decir que estamos diciendo que el array de los elementos que retorna isResponseData es una array de elementos undefined cuando viendo el código de esta función podemos estar seguros de que no es así. ¿Qué conclusión podemos sacar de esto? Pues que el Type Predicate que hemos utiliado en la definición de nuestro Type Guard sobreescribirá la inferencia de tipos que pueda llegar a hacer TypeScript:

Esto además es un problema desde el punto de vista del desarrollo porque mientras estamos construyendo nuestro software TypeScript nos dejará trabajar con payloads teniendo en cuenta que lo interpretará como un array de undefined pese a que esto no es realmente así siendo esta la razón por la que se recomienda que todos los Type Predicates que tengamos declarados en nuestro código siempre estén supervisados por tests.

Asertion Functions

Vamos a ver ahora otro caso que será la implementación de una Asertion Function para lo cual deberemos hacer uso de la palabra assert que nos proporciona TypeScript que puede transformar un Type Guard en una Type Assertion. Pero ¿cómo se hace esto en el código? Pues vamos a verlo en nuestro ejemplo:

function isResponseData(item: ResponseData | undefined): assert item is ResponseData {
}
Enter fullscreen mode Exit fullscreen mode

Y no solamente eso sino que además tendremos que cambiar la implementación de nuestro isResponseData porque en lugar de retornar un valor booleano deberemos lanzar un Error en el caso de que no se cumpla la condición que se quiere comprobar con la ejecución de la función. ¿Qué quiere esto decir? Pues que en nuestro caso el código de la Type Assertion sería algo como lo siguiente:

function isResponseData(item: ResponseData | undefined): assert item is ResponseData {
  if (item === undefined) {
    throw new Error('It is undefined')
  }
}
Enter fullscreen mode Exit fullscreen mode

De esta manera el compilador de TypeScript es capaz de deducir que en el caso de que se pase la vetificación que se lleva a cabo en el Type Assertion (es decir, que no se lanza un error), entonoces se tipo de datos que tendrá asociado será un ResponseData gracias a cómo está declarado el código dentro de la función.

Ahora bien, aunque esto es correcto no es una buena aproximación a seguir para ser utilizada como una argumento del método filter() puesto que lanzará un Error si no se cumple la condición y por lo tanto detendrá la ejecución del programa en el caso de que no sea capturado:

Nota: es importante tener en cuenta que los Type Assertions pueden lanzar errores en tiempo de ejecución y por lo tanto interrumpir la ejecución de nuestros programas mientras que los Type Guards lo que pueden llegar a provocar es que se produzcan errores en tiempo de construcción.

Por lo tanto ¿Cuándo deberíamos usar una Assertion Function? Pues sin duda alguna cuando tengamos que realizar algún tipo de validación en tiempo de ejecución mientras que los Type Guard son perfectos para cuando necesitamos hacer un narrowing type (extrechar el tipo de datos) durante el tiempo de compilación.

💖 💪 🙅 🚩
devjosemanuel
DevJoseManuel

Posted on April 30, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related