Type Guards & Type Assertions
DevJoseManuel
Posted on April 30, 2024
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
}
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' }]
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]
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
]
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)
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)
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
)
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 {}
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
}
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)
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
}
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 {
}
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')
}
}
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.
Posted on April 30, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.