TypeScript avanzado con un ejemplo real (Parte 1)

Publicado: 2020-11-06

La semana pasada vimos una pequeña introducción a TypeScript y, específicamente, hablamos sobre cómo este lenguaje que extiende JavaScript puede ayudarnos a crear un código más robusto. Como eso fue solo una introducción, no hablé sobre algunas de las características de TypeScript que quizás desee (y probablemente necesite) usar en sus proyectos.

Hoy te enseñaré a aplicar TypeScript de manera profesional en un proyecto real. Para ello, empezaremos mirando una parte del código fuente de Nelio Content para entender por dónde empezamos y qué limitaciones tenemos actualmente. A continuación, mejoraremos gradualmente el código JavaScript original agregando pequeñas mejoras incrementales hasta que tengamos un código completamente escrito.

Usando el código fuente de Nelio Content como base

Como ya sabrás, Nelio Content es un complemento que te permite compartir el contenido de tu sitio web en las redes sociales. Además de esto, también incluye varias funcionalidades que tienen como objetivo ayudarte a generar constantemente mejor contenido en tu blog, como un análisis de calidad de tus publicaciones, un calendario editorial para realizar un seguimiento del próximo contenido que necesitas escribir, etc. .

Calendario editorial de Nelio Content
Calendario editorial de Nelio Content.

El mes pasado publicamos la versión 2.0, un completo rediseño tanto visual como interno de nuestro plugin. Creamos esta versión utilizando todas las nuevas tecnologías que tenemos disponibles en WordPress hoy (algo de lo que hemos hablado recientemente en nuestro blog), incluida una interfaz React y una tienda Redux.

Entonces, en el ejemplo de hoy, mejoraremos este último. Es decir, veremos cómo podemos escribir una tienda Redux.

Selectores de Calendario Editorial de Nelio Content

El calendario editorial es una interfaz de usuario que muestra las publicaciones del blog que tenemos programadas para cada día de la semana. Esto significa que, como mínimo, nuestra tienda Redux necesitará dos operaciones de consulta: una que nos diga las publicaciones que están programadas en un día determinado y otra que, dada una ID de publicación, devuelva todos sus atributos.

Suponiendo que hayas leído nuestras publicaciones sobre el tema, ya sabes que un selector en Redux recibe como primer parámetro el estado con toda la información de nuestra aplicación seguido de cualquier parámetro adicional que pueda necesitar. Así que nuestros dos selectores de ejemplo en JavaScript serían algo como esto:

 function getPost( state, id ) { return state.posts[ id ]; } function getPostsInDay( state, day ) { return state.days[ day ] ?? []; }

Si te preguntas cómo sé que un estado tiene los atributos de posts y days , es bastante simple: porque soy yo quien los definió. Pero he aquí por qué decidí implementarlos de esta manera.

Sabemos que queremos poder acceder a nuestra información desde dos puntos de vista diferentes: publicaciones en un día o publicaciones por ID. Entonces parece que tiene sentido organizar nuestros datos en dos partes:

  • Por un lado, tenemos un atributo de posts en el que hemos enumerado todas las publicaciones que hemos obtenido del servidor y guardado en nuestra tienda Redux. Lógicamente, podríamos haberlos guardado en una matriz y hacer una búsqueda secuencial para encontrar la publicación cuyo ID coincida con el esperado… pero un objeto se comporta como un diccionario, ofreciendo búsquedas más rápidas.
  • Por otro lado, también necesitamos acceder a las publicaciones que están programadas para un día determinado. Nuevamente, podríamos haber usado solo una matriz para almacenar todas las publicaciones y filtrarlas para encontrar las publicaciones que pertenecen a un día determinado, pero tener otro diccionario ofrece una solución de búsqueda más rápida.

Acciones y Reductores en Nelio Content

Finalmente, si queremos un calendario dinámico, debemos implementar funciones que nos permitan actualizar la información que almacena nuestra tienda. Para simplificar, vamos a proponer dos métodos simples: uno que nos permite agregar nuevas publicaciones al calendario y otra que nos permite modificar los atributos de las existentes.

Las actualizaciones de una tienda Redux requieren dos partes. Por un lado, tenemos acciones que señalan el cambio que queremos realizar y, por otro, un reductor que, dado el estado actual de nuestra tienda y una acción solicitando una actualización, aplica los cambios necesarios al estado actual para generar un nuevo estado.

Entonces, teniendo esto en cuenta, estas son las acciones que podríamos tener en nuestra tienda:

 function receiveNewPost( post ) { return { type: 'RECEIVE_NEW_POST', post, }; } function updatePost( postId, attributes ) { return { type: 'UPDATE_POST', postId, attributes, } }

y aquí está el reductor:

 function reducer( state, action ) { state = state ?? { posts: {}, days: {} }; const postIds = Object.keys( state.posts ); switch ( action.type ) { case 'RECEIVE_NEW_POST'; if ( postIds.includes( action.postId ) ) { return state; } return { posts: { ...state.posts, [ action.post.id ]: action.post, }, days: { ...state.days, [ action.post.day ]: [ ...state.days[ action.post.day ], action.post.id, ], }, }; case 'UPDATE_POST'; if ( ! postIds.includes( action.postId ) ) { return state; } const post = { ...state.posts[ action.postId ], ...action.attributes, }; return { posts: { ...state.posts, [ post.id ]: post, }, days: { ...Object.keys( state.days ).reduce( ( acc, day ) => ( { ...acc, [ day ]: state.days[ day ].filter( ( postId ) => postId !== post.id ), } ), {} ), [ post.day ]: [ ...state.days[ post.day ], post.id, ], }, }; } return state; }

¡Tómate tu tiempo para entenderlo todo y sigamos adelante!

De JavaScript a TypeScript

Lo primero que debemos hacer es traducir el código anterior a TypeScript. Bueno, como TypeScript es un superconjunto de JavaScript, ya lo es… pero si copias y pegas las funciones anteriores en TypeScript Playground, verás que el compilador se queja bastante porque hay demasiadas variables cuyo tipo implícito es any . Así que arreglemos eso primero agregando explícitamente algunos tipos básicos.

Todo lo que tenemos que hacer es agregar explícitamente any tipo a cualquier cosa que sea "compleja" (como el estado de nuestra aplicación) y usar un number o string o lo que sea que queramos para cualquier otra variable/argumento. Por ejemplo, el selector de JavaScript original:

 function getPost( state, id ) { return state.posts[ id ]; }

con tipos explícitos de TypeScript se vería así:

 function getPost( state: any, id: number ): any | undefined { return state.posts[ id ]; }

Como puedes ver, la simple acción de teclear nuestro código (incluso cuando usamos “tipos genéricos”) ofrece mucha información con un rápido vistazo; ¡una clara mejora en comparación con JavaScript básico! En este caso, por ejemplo, vemos que getPost espera un number (una ID de publicación es un número entero, ¿recuerdas?) y el resultado será algo si la publicación existe ( any ) o nada si no existe ( undefined ).

Aquí tenéis el enlace con todo el tipo de código usando tipos simples para que el compilador no se queje.

Crear y usar tipos de datos personalizados en TypeScript

Ahora que el compilador está contento con nuestro código fuente, es hora de pensar un poco en cómo podemos mejorarlo. Para ello siempre propongo empezar modelando los conceptos que tenemos en nuestro dominio.

Creación de un tipo personalizado para publicaciones

Sabemos que nuestra tienda contendrá principalmente publicaciones, por lo que diría que el primer paso es modelar qué es una publicación y qué información tenemos sobre ella. Ya vimos cómo crear tipos personalizados la semana pasada, así que intentémoslo hoy con el concepto de publicación:

 type Post = { id: number; title: string; author: string; day: string; status: string; isSticky: boolean; };

No hay sorpresas aquí, ¿verdad? Una Post es un objeto que tiene algunos atributos, como una id numérica, un title de texto, etc.

Otra información importante que tiene cualquier tienda Redux es, lo adivinaste, su estado. En la sección anterior ya hemos discutido los atributos que tiene, así que definamos la forma básica de nuestro tipo de State :

 type State = { posts: any; days: any; };

Mejorar el tipo de State

Ahora sabemos que State tiene dos atributos ( posts y days ), pero no sabemos mucho acerca de lo que son, ya que pueden ser any cosa. Dijimos que queríamos que ambos atributos fueran diccionarios. Es decir, dada una determinada consulta (ya sea un ID de publicación para posts o una fecha para days ), queremos los datos relacionados (una publicación o una lista de publicaciones, respectivamente). Sabemos que podemos implementar un diccionario usando un objeto, pero ¿cómo representamos un diccionario en TypeScript?

Si echamos un vistazo a la documentación de TypeScript, veremos que incluye varios tipos de utilidades para hacer frente a situaciones bastante habituales. En concreto, hay un tipo llamado Record que parece ser el que queremos: nos permite teclear una variable mediante pares clave/valor en los que la clave tiene un determinado tipo Keys y los valores son de tipo Type . Si aplicamos este tipo a nuestro ejemplo, terminaríamos con algo como esto:

 type State = { posts: Record<number, Post>; days: Record<string, number[]>; };

Desde la perspectiva del compilador, el tipo Record funciona de tal manera que, dado cualquier valor de Keys (en nuestro ejemplo, number para posts y string para days ), su resultado siempre será un objeto de tipo Type (en nuestro caso, un Post o un number[] , respectivamente). El problema es que no es así como queremos que se comporte nuestro diccionario: cuando buscamos una publicación específica usando su ID, queremos que el compilador sepa que podemos encontrar o no una publicación relacionada, lo que significa que el resultado puede ser una Post o undefined .

Afortunadamente, podemos arreglar esto fácilmente usando otro tipo de utilidad, el tipo Partial :

 type State = { posts: Partial< Record<number, Post> >; days: Partial< Record<string, number[]> >; };

Mejorando nuestro código con alias de tipo

Echa un vistazo a los atributos de las posts en nuestro estado... ¿Qué ves? Un diccionario que indexa publicaciones tipo Post con números, ¿verdad? Ahora imagínese revisando este código en el trabajo. Si encuentra un tipo de este tipo, puede suponer que un number de publicaciones indexadas es probablemente la ID de las publicaciones indexadas... pero eso es solo una suposición; tendrías que revisar el código para estar seguro. ¿Y qué hay de los days ? "Cadenas aleatorias que indexan listas de números". Eso no es muy útil, ¿verdad?

Los tipos de TypeScript nos ayudan a escribir código más sólido gracias a las comprobaciones del compilador, pero ofrecen mucho más que eso. Si usa tipos significativos, su código estará mejor documentado y será más fácil de mantener. Entonces, agreguemos alias a los tipos existentes para crear tipos significativos, ¿de acuerdo?

Por ejemplo, sabiendo que las ID de publicación ( number ) y las fechas ( string ) son relevantes para nuestro dominio, podemos crear fácilmente los siguientes tipos de alias:

 type PostId = number; type Day = string;

y luego reescribir nuestros tipos originales usando estos alias:

 type Post = { id: PostId; title: string; author: string; day: Day; status: string; isSticky: boolean; }; type State = { posts: Partial< Record<PostId, Post> >; days: Partial< Record<Day, PostId[]> >; };

Otro tipo de alias que podemos usar para mejorar la legibilidad de nuestro código es el tipo Dictionary , que "oculta" la complejidad de usar Partial y Record detrás de una estructura conveniente:

 type Dictionary<K extends string | number, T> = Partial< Record<K, T> >;

haciendo nuestro código fuente más claro:

 type State = { posts: Dictionary<PostId, Post>; days: Dictionary<Day, PostId[]>; };

¡Y eso es! ¡Ahí tienes! Con solo tres alias de tipo simple, pudimos documentar el código de una manera que es claramente mejor que usar comentarios. Cualquier desarrollador que nos suceda podrá saber, de un vistazo, que posts es un diccionario que indexa objetos de tipo Post utilizando su PostId y que days es una estructura de datos que, dado un Day , devuelve una lista de identificadores de post. Eso es bastante impresionante, si me preguntas.

Pero no solo las definiciones de tipos son mejores... si usamos estos nuevos tipos en todo nuestro código:

 function getPost( state: State, id: PostId ): Post | undefined { return state.posts[ id ]; }

¡también se beneficia de esta nueva capa semántica! Puede ver la nueva versión de nuestro código escrito aquí.

Ah, por cierto, tenga en cuenta que los alias de tipo son, desde el punto de vista del compilador, indistinguibles del tipo "original". Esto significa que, por ejemplo, un PostId y un number son completamente intercambiables. Así que no espere que el compilador active un error si asigna un PostId a un number o viceversa (como puede ver en este pequeño ejemplo); simplemente sirven para añadir semántica a nuestro código fuente.

Próximos pasos

Como puede ver, puede escribir código JavaScript utilizando tipos de TypeScript de forma incremental y, al hacerlo, mejora su calidad y legibilidad. En el post de hoy hemos visto con cierto detalle un ejemplo de una implementación real de una aplicación React + Redux y hemos visto como se podría mejorar con relativamente poco esfuerzo. Pero nosotros aún tenemos un largo camino por seguir.

En la próxima publicación, escribiremos todas las variables/argumentos restantes que actualmente usan any tipo y también aprenderemos algunas hazañas avanzadas de TypeScript. Espero que les haya gustado esta primera parte y, si fue así, por favor compártanla con sus amigos y colegas.

Imagen destacada de Danielle MacInnes en Unsplash.