Guardianes del código

Como desarrolladores, no queremos utilizar el tiempo de nuestros sprints debuggeando bugs, y no queremos ser llamados a las 8 de la noche de un Sábado porque nuestro software está roto. Queremos tener confianza en lo que hacemos, necesitamos un héroe que nos resguarde y proteja de los errores que podrian ser fácilmente evitados. Necesitamos programar más defenisvamente! Necesitamos… contratos???

El comportamiento inesperado puede causar bugs

Contratos al rescate!

Cada vez que programamos definimos contratos, aún si no lo hacemos conscientemente, cuando definimos una función, definimos un contrato en el que declaramos los parámetros que recibe y la relación y restricciones que pueden tener. Cuando llamamos a una función, estamos aceptando sus “Términos y Condiciones”, y si es código de terceros el contrato seguramente esté definido en forma de documentación.

El método Array.indexOf es un excelente ejemplo. Como se describe en la documentación podemos usarlo para buscar si un elemento existe en el Array. Tiene dos posibles argumentos, el primero es obligatorio, y el segundo es opcional.

Método indexOf de los arrays de JavaScript

Vale la pena notar todas las condiciones relacionadas al segundo argumento. Qué pasa si no lo pasamos? Qué pasa si es un valor negativo? Qué pasa si es un número mayor al tamaño del Array? Qué pasa si cometemos un error y pasamos un string?

Si fuésemos a programar esta función nosotros mismos, probablemente haríamos algo como esto:

Donde definimos pre-condiciones que nuestros parámetros deben satisfacer antes de hacer el trabajo en sí. Estas pre-condiciones son también conocidas como Guards, como podemos ver en la definición en Wikipedia:

En programación, un guard es una expresión booleana que debe ser evaluada como verdadera para que el programa continúe por la rama en cuestión. Independientemente del lenguaje de programación usado, una clausula de guard es un chequeo de integridad llamado precondición, y es usado para evitar errores durante la ejecución.

Para hacer que nuestros contratos sean más seguros podríamos también definir post-condiciones, que como su nombre implican, son ejecutadas después del código de la función. En el caso de indexOf podríamos asegurarnos de que la respuesta es un número que represente la posición del array o el valor -1.

Tanto las pre y post condiciones nos dan garantías sobre cómo programamos. Las primeras protejen a nuestras funciones de quien las llame, y las segundas protejen al código de sí mismo.


TypeScript A Todo Ritmo

Una de las ventajas de usar guards es que siguen el principio de fallar lo más rápido posible. En vez de dejar que nuestros errores se propaguen o sean ignorados silenciosamente, nos van a tirar una advertencia apenas vean el error y nos ayudarán a encontrar el origen del problema.

Para fallar aún más rápido, podemos usar un chequeo de tipos estáticos como TypeScript. Funciona como la policía de nuestros programas, enforzando los contratos que definimos en tiempo de compilación. En el siguiente ejemplo refactorizamos una de las pre-condiciones usando anotación de tipos:

TypeScript te obliga a cumplir los contratos 👮

Si llamamos a la función y no respetamos su contrato vamos a tener un error en tiempo de compilación, lo que significa ciclos de desarrollo más rápidos, ya que no tenemos que probar el código si tenemos un error, el compilador nos dará una pista de qué tenemos que arreglar.

Nuestra complejidad mental claramente ha aumentado, ya que entender el concepto de generics es más complicado que una sentencia if, pero ganamos más garantías. Una pregunta que no nos hicimos antes es: qué pasa si tenemos una lista de números [1, 2, 3] y buscamos por un string, por ejemplo “3”? La respuesta probablemente sea lo esperado, recibimos un -1.

Si no usamos un type checker, no nos daremos cuenta que estamos violando el contrato de indexOf y tendremos un bug silencioso (que no tira un error). Si usamos TypeScript tendremos un error en tiempo de compilación que nos hará reflexionar sobre la verdadera pregunta: Por qué estamos buscando un string en un array de números en primer lugar?

También podemos agregar un tipo de retorno a nuestra función para que funcione como una post-condición, enforzandonos a devolver un número. Vale la pena notar que no todos los type systems son creados iguales, y te pueden dar distintas garantías. Por ejemplo, TypeScript no puede obligarte a devolver un número en el rango correcto, mientras que un lenguaje más restrictivo con el concepto de Dependent Types, como IDRIS, sí puede.


Type guards

TypeScript tiene un concepto llamado Type Guards que está relacionado con lo que venimos hablando, pero en vez de continuar o no la ejecución del programa, va a poder achicar el tipo inferido de una variable en un scope dependiendo de algunos chequeos dinámicos.

Consideremos el siguiente código:

Si lo corremos en la consola de JavaScript y el elemento existe, todo estará bien, pero si no existe tendremos el error en tiempo de ejecución: Cannot set property ‘innerHTML’ of null.

Si usamos TypeScript con la opción strictNullCheck habilitada, la función getElementById va a devolver la unión de tiposHTMLElement | null”. Entonces no importa si el elemento existe o no, tendremos un error en tiempo de compilación diciendo que el objeto tal vez sea null.

Agregando un simple if podemos solucionar el error:

Esto es posible gracias a los Type Guards. TypeScript usa “análisis de flujo” para tratar de entender todos los posibles tipos que una variable puede tener en distintas partes del código. En la primera linea, header puede ser “HTMLElement | null”, la única forma de llegar a la línea 4 es si header es thrutly, así que TypeScript va a achicar su tipo a HTMLElement. De la misma manera, la única forma de llegar a la línea 6 es si el header es falsy, entonces el tipo será null.

Los Type Guards también pueden ser usados para definir “tipos de datos algebraicos” (ADT) para separar la lógica de los datos. Examinemos el siguiente código:

Adentro de la función doYourThing no sabemos mucho sobre animal. Sabemos que tiene una propiedad llamada species, ya que tanto Cat y Dog tienen una propiedad con ese nombre. El tipo de la propiedad será la unión de los posibles tipos, en este caso ‘cat’ | ‘dog’.

La única forma de llegar a la línea 19 es si species es igual a ‘dog’, asi que el Type Guard va a achicar el tipo de animal a Dog en ese bloque, permitiendonos usar la propiedad favoriteHuman. De la misma manera, si llegamos a la línea 21 es porque animal es del tipo Cat, por lo que tendrá la propiedad nemesis.

No deberiamos poder llegar a la línea 23, porque species sólo puede ser ‘cat’ o ‘dog’ y ambas condiciones fueron chequeadas dentro de la función, entonces el Type Guard concluye que en ese bloque el tipo de animal es never. Esto nos permite hacer algo llamado chequeo exhaustivo, un patrón que nos obliga a contemplar todos los valores posibles del ADT. Si agregamos un nuevo tipo de Animal en la línea 11 y no modificamos nuestra función, obtendremos un error porque no le podremos pasar ese animal a la función assertNever.

En esta charla podemos ver una demostración de este feature de la mano de Anders Hejlsberg, uno de los desarrolladores principales del lenguaje.


Custom Type Guards

TypeScript viene con una variedad de Type Guards ya predefinidos. Tenemos instanceOf, typeOf, y los literal values directamente disponibles, pero hay algunos escenarios donde necesitamos definir nuestros propios Type Guards.

El concepto que usamos en la sección anterior para crear nuestros ADTs se llama unión discriminada, en donde nos aprovechamos que todos los tipos de la unión comparten una misma propiedad con valores distintos que nos permite saber el tipo de nuestro objeto. Pero qué pasa si los tipos no son tan homogéneos?

Este código nos tirará un error de compilación en la línea 15, ya que TypeScript no puede asumir que feline tiene una propiedad llamada species. Si tratamos de darle una pista al compilador, casteando el valor a Cat, romperemos el Type Guard, y eso nos tirará un error en la función assertNever.

Para hacer que este ejemplo funcione tenemos que crear un Type Guard custom, que no es nada más que una función que devuelve un booleano. La sintaxis es bastante fácil, recibis un parámetro x con un tipo más general y devolves x is foo para indicar que si la condición es cierta, entonces x es del tipo foo.

Y ahora nuestro ejemplo funciona correctamente 👍. Si queres ver un ejemplo real de cómo se pueden usar los custom type guards, podes fijarte como usamos la función variadica isInstanceOf y caseError para manejar correctamente nuestros errores usando Task. Acá les dejo un vídeo de una lightning talk que di en Meetup.js en el que se ve el que muestro todo eso 😉.


Eso es todo por ahora! Díganme que les pareció el artículo en twitter @sherman3ero, todo el feedback es bienvenido❤️.

Generalmente escribo sobre TypeScript y programación funcional. Por favor díganme los temas que les gustaría leer.

Saludos 👋