Composición de tipos en Typescript

21-09-2023

Por Adrián Ferrera González

Introducción

Uno de los puntos fuertes de Typescript, es la flexibilidad que nos brinda en el momento de definir estructuras de datos.

Esta misma flexibilidad puede ser, al mismo tiempo, la mayor fuente de problemas en las manos inadecuadas, como podremos ver a continuación.

Historia

Hace algunas semanas definimos en nuestra aplicación un tipo llamado UserAddress:

export interface UserAddress { firstName: string middleName: string lastName: string email: string address: Address }

Tal y como se muestra, esta interfaz define las propiedades referentes a un usuario y la dirección del mismo.

Todo iba bien, hasta que comenzaron a surgir requisitos por parte de la aplicación, donde la dirección de un usuario podía ser una dirección para Billing o para Shipping. ¿Qué implicaciones tiene esto? A priori ninguna.

Sin embargo, pocos días después fuimos notificados de que a su vez, Shipping podía ser tanto físico como digital.

Al par de días haciendo una CR vimos algo extraño: estábamos teniendo un error en la aplicación, ya que se daban casos muy extraños donde se trataba de una dirección de shipping digital, pero estaban presentes los datos de shipping físicos.

Al revisar el código nos encontramos lo siguiente:

export interface UserAddress { firstName: string middleName?: string lastName: string email?: string activationEmail?: string address?: Address }

Ahora bien, como solución es totalmente válida, sin embargo, el tratar de utilizar un mismo tipo de forma genérica, y marcando sus propiedades como opcionales, puede incurrir en muchos errores como era el caso.

¿Cómo lo podemos solucionar? Siendo más específicos. Partamos de una base: ¿Qué tipos son esenciales para definir la dirección de un usuario? Pues en primer lugar, los datos del usuario (en este caso Customer):

export type Customer = { firstName: string middleName?: string lastName: string email: string }

¿Qué caracteriza a una dirección digital? Se trata de un Customer, pero en lugar de tener un campo email, posee un campo activationEmail. Esto puede ser definido de la siguiente manera:

export type UserDigitalAddress = Omit<Customer, 'email'> & { activationEmail: string }

Y por su parte, una dirección física se caracteriza por tener la información de la calle, número, país (envuelto en el tipo Address):

export type UserPhysicalAddress = Customer & { address: Address }

Por último, se nos da el caso de que una misma compra, puede ser al mismo tiempo: física, si el envío se tiene que enviar a un domicilio; o digital, si únicamente se requiere activar en una cuenta. De ser así, podemos afirmar lo siguiente:

export type UserDigitalPhysicalAddress = UserDigitalAddress & UserPhysicalAddress

Teniendo todos estos tipos, ahora podremos agruparlo en uno, el cual conocerá las restricciones de cada uno, y admitirá que se use cualquiera de sus definiciones, siempre que cumpla la definición de una de ellas:

export type UserShippingAddress = UserDigitalAddress|UserPhysycalAddress|UserDigitalShippingAddress

Por consiguiente, podemos decir que tanto la dirección de Billing y de Shipping cumplen la siguiente definición:

export type UserShippingAddress = UserDigitalAddress|UserPhysycalAddress|UserDigitalShippingAddress export type UserBillingAddress = UserPhysicalAddress

Pudiendo ser usada de la siguiente manera:

const checkout = (billing: UserBillingAddress, shipping: UserShippingAddress , customer: Customer) => { // ... }

Conclusión

El uso de tipos avanzados no es una solución fija, pero en muchos casos puede protegernos de casos imposibles, que se darán si empezamos a definir todas nuestras propiedades como opcionales. Por consiguiente, nuestro código será más semántico, y en el momento de desarrollo, podremos apoyarnos en estos tipos para estar seguros de que el funcionamiento es el esperado.

Resultado final:

export type Customer = { firstName: string middleName?: string lastName: string email: string } export type UserDigitalAddress = Omit<Customer, 'email'> & { activationEmail: string } export type UserPhysicalAddress = Customer & { address: Address } export type UserDigitalPhysicalAddress = UserDigitalAddress & UserPhysicalAddress export type UserShippingAddress = UserDigitalAddress|UserPhysicalAddress|UserDigitalPhysicalAddress export type UserBillingAddress = UserPhysicalAddress