leanmind logo leanmind text logo

Blog

Refactorización Avanzada

Dominar el refactoring productivo para maximizar el retorno de la inversión.

Composición de tipos en Typescript

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 
Publicado el 21/09/2023 por
Adrián image

Adrián Ferrera González

https://adrianferrera.dev

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers