Implementación de interfaces en JavaScript con Implement.js

En este artículo, presentaré el concepto de interfaces y cómo pueden ser útiles incluso en lenguajes dinámicos. También usaré la biblioteca Implement.js para llevar el concepto a JavaScript, y le mostraré cómo obtener alguna utilidad adicional de las interfaces.

¿Qué es una interfaz?

Google define una interfaz como "un punto donde dos sistemas, sujetos, organizaciones, etc. se encuentran e interactúan", y esta definición es válida para las interfaces en programación. En el desarrollo de software, una interfaz es una estructura que impone propiedades específicas en un objeto; en la mayoría de los lenguajes, este objeto es una clase.

Aquí hay un ejemplo de una interfaz en Java:

En el ejemplo anterior, la interfaz de Car describe una clase que tiene dos métodos sin tipo de retorno, y ambos toman un único argumento de entero. Los detalles de la implementación de cada función se dejan a la clase, esta es la razón por la cual los métodos no tienen cuerpo. Para garantizar que una clase implemente la interfaz de Car , utilizamos la palabra clave implements :

Sencillo

Interfaces en JavaScript

Las interfaces no son algo en JavaScript, en realidad no de todos modos. JavaScript es un lenguaje dinámico, en el que los tipos se cambian tan a menudo que el desarrollador puede no haberse dado cuenta, debido a esto las personas argumentan que no hay necesidad de agregar una interfaz al estándar ECMAScript en el que se basa JavaScript.

Sin embargo, JavaScript ha crecido enormemente como un lenguaje de back-end en la forma de Node.js, y con eso vienen diferentes requisitos y una multitud diferente que puede tener una opinión diferente. Para agregar a esto, el lenguaje se está convirtiendo rápidamente en la herramienta de entrada; ha llegado al punto en que muchos desarrolladores escriben la gran mayoría de su HTML dentro de archivos .js en forma de JSX.

A medida que el lenguaje crece para asumir más roles, es útil asegurarse de que una de nuestras estructuras de datos más cruciales sea la que esperábamos que fuera. JavaScript puede tener la palabra clave de la class , pero a decir verdad esta es solo una función de constructor desinstalada, y una vez llamada simplemente es un objeto. Los objetos son omnipresentes, por lo que a veces es beneficioso asegurarse de que coincidan con una forma específica.

Recientemente, en el trabajo, encontré un caso en el que un desarrollador esperaba que una propiedad devuelta como parte de una respuesta de la API fuera true pero que en cambio era "true" , lo que provocaba un error. Un error fácil, y que también podría haberse evitado si tuviéramos una interfaz.

¡Pero espera hay mas!

Las interfaces, con algunas modificaciones menores, podrían usarse para remodelar objetos. Imagine implementar una interfaz "estricta", donde no se permiten propiedades fuera de la interfaz, podríamos eliminar o cambiar el nombre de estas propiedades, o incluso lanzar un error si las encontramos.

Entonces ahora tenemos una interfaz que nos dirá cuando nos faltan ciertas propiedades, pero también cuando tenemos propiedades inesperadas, o si los tipos de propiedades no son los que esperamos. Esto agrega otras posibilidades, por ejemplo, refactorizar una respuesta de una API al tiempo que agrega esa capa adicional de seguridad sobre el comportamiento estándar de la interfaz. También podríamos usar interfaces en pruebas unitarias si hacemos que arrojen errores.

Implement.js

Implement.js es una biblioteca que intenta traer interfaces a JavaScript. La idea es simple: defina una interfaz, defina los tipos de sus propiedades y úsela para asegurarse de que un objeto es lo que espera que sea.

Preparar

Primero instala el paquete:

 npm instala implemente-js 

A continuación, cree un archivo .js e importe implement , Interface y type :

Nuestra primera interfaz

Para crear una interfaz, simplemente llame a Interface y pase una cadena como el nombre de su interfaz; no es recomendable, pero si omite el nombre, se generará una ID única. Esto devuelve una función que acepta un objeto donde las propiedades son todos los objetos de type , un segundo argumento también se puede pasar con opciones para mostrar advertencias, lanzar errores, eliminar o renombrar propiedades, asegurar que solo estén presentes las propiedades de la interfaz, o extender una Interface existente.

Aquí hay una interfaz que describe un automóvil:

Tiene una propiedad de seats que debe ser de tipo número, una matriz de pasajeros que contiene objetos que deben implementar ellos mismos la interfaz Passenger , y contiene una propiedad de beep , que debería ser una función. El error y strict opciones strict se han establecido en verdadero, lo que significa que se generarán errores cuando falte una propiedad de la interfaz y también cuando se encuentre una propiedad que no se encuentre en la interfaz.

Implementando nuestra interfaz

Ahora queremos implementar nuestra interfaz, en un ejemplo simple crearemos un objeto usando un literal de objeto y veremos si podemos implementar nuestra interfaz.

Primero, creamos un objeto Ford , luego intentaremos implementarlo contra nuestra interfaz Car :

Como vemos en el comentario anterior, esto arroja un error. Echemos un vistazo a nuestra interfaz de Car :

Podemos ver que, si bien todas las propiedades están presentes, el modo estricto también es cierto, lo que significa que la propiedad adicional tipo de fuelType causa un error. Además, aunque tenemos una propiedad para passengers , no es una matriz.

Para implementar correctamente la interfaz, eliminamos fuelType y cambiamos el valor de los passengers para que sea una matriz que contiene objetos que implementan la interfaz Passenger :

"¡Pero JavaScript no es un lenguaje orientado a objetos!"

Es cierto que, si bien las interfaces suelen estar asociadas a lenguajes orientados a objetos, y JavaScript es un lenguaje de múltiples paradigmas que utiliza la herencia prototípica, las interfaces aún pueden ser muy útiles.

Por ejemplo, al usar implement-js podemos refactorizar fácilmente una respuesta de la API y asegurarnos de que no se haya desviado de lo que esperamos. Aquí hay un ejemplo usado junto con redux-thunk :

En primer lugar, definimos la interfaz TwitterUser , que amplía la interfaz de User , como un objeto con propiedades de twitterId y twitterUsername . trim es verdadero, lo que significa que descartaremos cualquier propiedad no descrita en la interfaz TwitterUser . Dado que nuestra API devuelve propiedades en un formato antipático, hemos cambiado el nombre de las propiedades de twitter_username y twitter_id a versiones de camelcase de ellos mismos.

A continuación, definimos una acción asíncrona con redux-thunk , la acción desencadena una llamada API, y usamos nuestra interfaz TwitterUser para descartar las propiedades que no queremos y para asegurarnos de que implemente las propiedades que esperamos, con los tipos correctos. Si prefiere mantener sus creadores de acciones puros (er) o no use redux-thunk , le redux-thunk verificar la interfaz dentro de twitterService.getUser y devolver el resultado.

Nota: cuando se extienden las opciones de una interfaz no son heredadas

Las pruebas unitarias también son un lugar adecuado para usar interfaces:

En resumen

Hemos visto cómo las interfaces pueden ser útiles en JavaScript: a pesar de que es un lenguaje altamente dinámico, verificar la forma de un objeto y que sus propiedades son un tipo de datos específico nos da una capa adicional de seguridad que de otro modo nos estaríamos perdiendo. Al construir sobre el concepto de interfaces y usar implement-js , también hemos podido obtener una utilidad adicional además de la seguridad adicional.