Uso avanzado de TypeScript en un ejemplo real (parte 1)

Publicada en WordPress.

Mira nuestro vídeo

Existe una versión mejor de tu web

Comparte este artículo

La semana pasada vimos una pequeña introducción a TypeScript y, en concreto, hablamos de cómo este lenguaje que extiende a JavaScript nos puede ayudar a crear un código más robusto. Ahora bien, al tratarse de una simple introducción, dejé por explicar un montón de características de TypeScript que puedes (y probablemente necesites) usar en tus proyectos.

Hoy te enseñaré cómo aplicar TypeScript de forma profesional en un proyecto real. Para ello, empezaremos viendo parte del código fuente de Nelio Content para entender de dónde partimos y qué limitaciones tenemos. A continuación, iremos mejorando poco a poco el código JavaScript original añadiendo pequeñas mejoras incrementales al código hasta tenerlo completamente tipado.

Un vistazo al código fuente de Nelio Content como ejemplo

Como supongo que ya sabes, Nelio Content es un plugin que permite compartir el contenido de tu web en las redes sociales. Además de ello, incluye también varias funcionalidades que tienen como objetivo ayudarte a generar de forma constante mejores contenidos en tu blog, como un análisis de calidad de entradas, un calendario editorial o acceso a feeds de otros blogs para inspirarte.

El calendario editorial de Nelio Content
El calendario editorial de Nelio Content.

El mes pasado publicamos la versión 2.0 de Nelio Content, la cual supuso un rediseño completo tanto a nivel visual como interno de nuestro plugin. Esta versión la creamos usando todas las nuevas tecnologías que tenemos disponibles en WordPress y que te hemos estado explicando en entradas anteriores, entre las que destacan una interfaz basada en React y una gestión del estado basada en Redux.

Selectores del calendario editorial de Nelio Content

El calendario editorial es una interfaz de usuario que muestra las entradas de nuestro blog (junto a otras cosas que podemos ignorar para este ejemplo) que tenemos programados para cada día de la semana. Esto quiere decir que, como mínimo, nuestro almacén Redux necesitará dos operaciones de consulta: una que nos diga qué entradas hay en un cierto día y otra que, dada una entrada cualquiera, nos permita obtener todos sus atributos.

Como imagino que ya sabes (porque doy por sentado que has leído nuestras entradas sobre el tema), un selector en Redux recibe como primer parámetro el estado con toda la información seguido de cualquier parámetro adicional que necesitemos. Así que nuestros dos selectores de ejemplo serían algo tal que así:

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

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

Claro, en este caso te puedes estar preguntando… ¿y cómo sabes tú qué pinta tiene el estado en cuestión? Y la respuesta es: porque lo defino yo, por supuesto. Pero tranquilo, que te lo explico.

Sabemos que queremos poder acceder a nuestra información desde dos puntos de vista diferentes: las entradas de un día o una entrada a través de su ID. Así que parece que tiene sentido organizar nuestros datos en dos partes:

  • Por un lado, tenemos un atributo posts en el que tenemos listadas todas las entradas que hemos obtenido del servidor y guardado en nuestro almacén Redux. Lógicamente, podríamos haberlas guardado en un array y hacer una búsqueda secuencial para encontrar la entrada cuyo ID coincide con el que nos dan… pero un objeto nos permite acceder directamente a una entrada concreta usando su ID.
  • Por otro lado, vemos que tenemos un atributo days que usamos como un diccionario para saber qué entradas hay en cada día de la semana. Como sé que necesitaremos obtener las entradas que aparecen en un cierto día, parece una optimización lógica tener esa información directamente disponible en el almacén y, así, ahorrarnos el tener que mirar todas las entradas una a una para ver si pertenece o no al día que nos piden.

Acciones y reductores en Nelio Content

Finalmente, si queremos que nuestro calendario no sea estático, debemos implementar funciones que nos permitan actualizar la información que tenemos delante de nosotros. Por simplicidad, vamos a proponer dos métodos sencillos: uno que nos permita añadir nuevas entradas al calendario y otra que nos permita modificar los atributos de las que existen.

Las actualizaciones en un almacén Redux se componen de dos partes. Por un lado están las acciones que señalan el cambio que queremos hacer y, por otro, el reductor que, dado el estado actual y una acción, aplica los cambios necesarios para generar un nuevo estado.

Pues bien, las acciones de nuestro ejemplo serían tal que así:

function receiveNewPost( post ) {
  return {
    type: 'RECEIVE_NEW_POST',
    post,
  };
}

function updatePost( postId, attributes ) {
  return {
    type: 'UPDATE_POST',
    postId,
    attributes,
  }
}

y el reductor sería parecido a esto:

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;
}

¿Todo claro? ¡Pues vamos a mejorar todo esto!

Pasando el código a TypeScript

Lo primero que podemos hacer es llevarnos todo este código a TypeScript. Si copias y pegas todas las funciones anteriores en el Playground de TypeScript, verás que el compilador se queja bastante porque hay demasiadas variables cuyo tipo implícito es any. El primer paso, pues, es darle una repasada a todo el código e intentar definir los tipos básicos para que todo esto funcione y no se queje.

Para ello, lo único que haremos es añadir explícitamente el tipo any a cualquier cosa que sea «complicada» (como el estado de nuestra aplicación) y el tipo básico de todo lo demás. Por ejemplo, el selector original JavaScript:

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

podría quedar tipado de la siguiente forma en TypeScript:

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

Como puedes ver, el simple hecho de poner los tipos (incluso cuando no son muy precisos) en la función nos ayuda a obtener mucha información sobre la misma simplemente leyendo su cabecera. En concreto, vemos que getPost espera que le demos un número (recordemos que el ID de una entrada es un número) y el resultado puede ser o bien any o bien undefined. Es decir, ya sabemos que esta función puede que nos de una entrada (any) o no (undefined).

Te dejo el enlace con todo el código tipado de forma básica para que el compilador no rechiste.

Crear y usar tipos de datos básicos en TypeScript

Ahora que ya tenemos un código que compila es hora de pensar un poco cómo podemos mejorarlo. Para ello, yo siempre propongo empezar por modelar los conceptos que tenemos en nuestro dominio de actuación.

Tipando nuestras entradas

En este caso, tenemos un almacén de datos que tiene como objetivo almacenar entradas, así que parece que tiene sentido definir cuál es la forma que debe tener una entrada:

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

La otra pieza extremadamente importante en un almacén Redux es, lógicamente, el estado. Ya hemos comentado que nuestro estado es un objeto con dos atributos (posts y days), así que vamos a representarlo:

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

Tipando el estado

Ahora bien, esto es insuficiente. Nosotros no queremos que posts y days puedan ser cualquier cosa (any); lo que queremos es que sean objetos que vamos a usar como diccionarios. ¿Cómo podemos representar en TypeScript un diccionario? Sabemos que un diccionario se puede representar como un objeto cuyas claves son variables y cuyos valores tienen un tipo conocido pero… ¿escribir esto en TypeScript?

Si echamos un vistazo a la documentación de TypeScript, veremos que incluye varios tipos de utilidades para lidiar con situaciones bastante comunes. En concreto hay un tipo llamado Record que parece ser el que queremos: nos permite tipar una variable usando pares clave/valor en los que la clave tiene un cierto tipo Keys y los valores son de tipo Type. Así, por ejemplo, parece que podemos resolver el problema de la siguiente forma:

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

Fíjate en una cosa interesante: para el compilador, el tipo Record funciona de tal forma que, dado un valor cualquiera de Keys (en nuestro ejemplo, number para posts y string para days), siempre vamos a obtner un resultado de tipo Type (en nuestro caso, o Post o number[]). El problema es que en nuestro caso esto no es cierto: cuando buscamos, por ejemplo, una entrada a través de su ID, esta puede que esté o no en el diccionario.

Dado que queremos que el compilador sepa que cuando accedemos a un índice de nuestros diccionarios el resultado puede ser «nada», lo que debemos hacer es usar otro tipo de utilidad, Partial, que nos permite expresar precisamente eso:

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

Mejorando los tipos usando alias

Echa un vistazo al atributo posts de nuestro estado, ¿qué ves? Un diccionario que indexa entradas de tipo Post con números, ¿no? Si estuvieras revisando este código que escribió un compañero hace unos meses, probablemente asumas que ese number se corresponde con el ID de la entrada… pero, vaya, eso no lo pone en ningún lado y tendrías que tirar del hilo y ver más código para comprobar que eso realmente es así. Y eso por no hablar de days, que indexa listas de números usando cadenas de texto… ¡a saber qué narices es eso!

Los tipos de TypeScript no solo nos permiten escribir un código más robusto gracias a todos los checks del compilador, sino que también nos permiten crear código que esté mejor documentado y sea más fácil de mantener. Para ello, basta con crear tipos nuevos que sean alias de tipos ya existentes.

Por ejemplo, sabiendo que los identificadores de entradas (number) y las fechas en las que está programada una entrada (string) son relevantes para nuestro dominio, podemos generar los siguientes dos 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[]> >;
};

Y si queremos mejorar un poco más la legibilidad de nuestro estado, en el que usamos una combinación de Partial y Record, pues lo mismo nos podemos crear un alias Dictionary como el siguiente:

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

para poder escribir el estado tal que así:

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

Y, sin hacer apenas nada, de repente el código fuente que hemos producido es muchísimo más fácil de entender y está mejor documentado (sin ni siquiera añadir un solo comentario). Ahora, de un vistazo, sabemos que posts indexa objetos de tipo Post usando su PostId y también sabemos que days es una estructura de datos que, dado un Day devuelve una lista de identificadores de entrada.

Si ahora usamos todos estos nuevos tipos a lo largo y ancho de nuestro código, tenemos resultados como el siguiente:

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

en el cual queda extremadamente claro qué es cada cosa y qué podemos esperar de una función. De hecho, si en esta función nos hubiéramos olvidado de especificar que el valor de retorno puede ser undefined, TypeScript se hubiera quejado y nos hubiéramos ahorrado un error en tiempo de ejecución.

De nuevo, te dejo un enlace con el ejemplo entero arreglado.

Por cierto, ten en cuenta que los tipos alias son, desde el punto de vista del compilador, indistinguibles al tipo «original». Esto quiere decir que, por ejemplo, un PostId y un number son totalmente intercambiables. No esperes, pues, que el compilador detecte un fallo ahí (tal y como puedes ver en este pequeño ejemplo); simplemente sirven para añadir semántica a nuestro código fuente.

Próximos pasos

Como puedes ver, ir tipando poco a poco un código JavaScript usando las diferentes funcionalidades de TypeScript mejora sustancialmente su calidad, tanto a nivel técnico como a nivel humano. En la entrada de hoy hemos visto con cierto detalle un ejemplo de una implementación real de una aplicación React+Redux y hemos visto cómo se podía mejorar con relativamente poco esfuerzo. Pero aún nos queda mucho por acabar.

En la próxima entrada acabaremos de resolver todas las variables que nos han quedado colgadas como any en el código y veremos aún más funcinalidades avanzadas de TypeScript. Espero que te haya gustado y, si así ha sido, ya sabes: compártela. Y si tienes dudas, déjamelas en la zona de comentarios y te ayudo.

Imagen destacada de Danielle MacInnes en Unsplash.

Deja una respuesta

No publicaremos tu correo electrónico. Los campos obligatorios están marcados con: •

He leído y acepto la Política de privacidad de Nelio Software

Al marcar la casilla de aceptación estás dando tu legítimo consentimiento para que tu información personal se almacene en SiteGround y sea usada por Nelio Software con el propósito único de publicar aquí este comentario. Contáctanos para corregir, limitar, eliminar o acceder a tu información.