¿Cómo utilizar las Uniones e Intersecciones en Typescript?

Una característica muy útil de Typescript son las uniones(unions) e intersecciones (intersections).

Estas 2 funcionalidades permiten manipular tipos de datos ya existentes con los operadores lógicos AND y OR.

Veamos un ejemplo. Supongamos que tenemos un videojuego y en el mismo tenemos el siguiente código para las armas que se pueden usar.

export enum WeaponType {
    SHORT_RANGE,
    LONG_RANGE,
}

export type Weapon = {
    price: number, // El precio
    power: number, // El poder de ataque
    range: number, // El alcance en valor numérico
    type: WeaponType // El tipo de arma (corto o largo alcance)
}

Intersecciones

Las intersecciones ayudan a combinar los tipos de datos de diferentes modelos, utilizando el símbolo ” & “.

Supongamos que queremos un tipo de dato que represente algo más específico como una pistola.

export type Gun = Weapon & {
    bulletCapacity: number, // El número de balas que puede tener cargadas
    fire: () => void // La acción de disparar
}

De esta manera nuestro tipo de dato Gun contiene las 4 propiedades que tiene Weapon más las 2 que específicamos en el objeto a la derecha del símbolo.

Por detrás, Typescript esta manejando Gun de esta manera:

type Gun = {
    price: number, // El precio
    power: number, // El poder de ataque
    range: number, // El alcance en valor numérico
    type: WeaponType, // El tipo de arma (corto o largo alcance)
    bulletCapacity: number, // El número de balas que puede tener cargadas
    fire: () => void // La acción de disparar
}

Podemos hacer lo mismo si queremos crear un tipo de dato para una espada.

export type Blade = Weapon & {
    bladeLength: number, // El largo de la espada
    brandish: () => void // La acción de blandir la espada
}

A su vez podemos usar los tipos de datos anteriores para Gun y Blade para crear un tipo de dato que represente un arma una fusión de ambas: Gunblade

export type Gunblade = Gun & Sword

Cómo ya te imaginarás Gunblade contiene todo lo que tenía Gun más todo lo que tenía Blade. La intersección por defecto omite los duplicados así que no tienes que preocuparte de que el contenido de Weapon se duplique o cause errores.

type Gunblade = {
    price: number, // El precio
    power: number, // El poder de ataque
    range: number, // El alcance en valor numérico
    type: WeaponType, // El tipo de arma (corto o largo alcance)
    bulletCapacity: number, // El número de balas que puede tener cargadas
    fire: () => void, // La acción de disparar
    bladeLength: number, // El largo de la espada
    brandish: () => void // La acción de blandir la espada
}

Uniones

Las uniones por otra parte ayudan a manejar varios tipos de datos a la vez utilizando el símbolo ” | ” .

Supongamos que en el juego hay tiendas donde puedes vender tus armas pero por alguna razón los dueños no les interesa ninguna arma blanca.

Además adicional a las armas que ya existen también hay otra para representar una bomba.

export type Bomb = Weapon & {
    explosionRadius: number, // El radio de la explosión de la bomba
    throw: () => void // La acción de lanzar la bomba
}

Teniendo esto en cuenta podemos tener una función que solo te permita vender ciertos tipos de armas

const sellWeapon = (weapon: Gun | Bomb) => {
    console.log(`Remove weapon from inventory: ${weapon} and get money equal to price`)
}

Evidentemente no necesitamos tener la implementación de la función para este ejemplo. Lo importante aquí es que te fijes en el parámetro weapon, el cual puede aceptar ya sea una pistola o una bomba, pero no una espada en este caso.

A diferencia de las intersecciones es poco común que necesites utilizar una unión en más de un lugar por lo que es normal ver que se usa sin asignarse a un nuevo tipo de dato. No obstante nada te impide hacerlo si sientes que se ajusta más a tus necesidades o a tu estilo de programación.

export type SellableWeapon = Gun | Bomb
const sellWeapon = (weapon: SellableWeapon ) => {
    console.log(`Remove weapon from inventory: ${weapon} and get money equal to price`)
}

Casos de Uso Reales

Siempre he opinado que esta clase de ejemplos ayudan mucho a entender la teoría. Pero siendo sincero es difícil visualizar una aplicación real en desarrollo web basandose en estos escenarios. Por lo que también creo que es valioso que te comparta la forma más común en que he aplicado estos conceptos en mi día a día.

Manejando las respuestas de un API.

Como bien sabes (y sino ahora lo sabrás), es bastante común que como desarrollador tengas que trabajar con un API construida por otra persona. Esto hace que no tengas control sobre ese código y lamentablemente es normal encontrar formatos de respuesta incosistentes en un API.

Dicho esto, este ese el procedimiento que suelo seguir:

Encuentro todas las propiedades en común que tienen las respuestas del API y creo un tipo de dato en base a eso.

export type ErrorResponse = {
    errCode: number,
    date: string
}

Defino las variantes de tipos de datos para las diferentes respuestas posibles usando intersecciones.

export type ServerErrorResponse = ErrorResponse  & {
    error: string
}

export type ClientErrorResponse = ErrorResponse  & {
    body: {
        message: string,
        description: string
    }
}

Utilizó las uniones para manejar el llamado de las respuesta de la petición.

const handleErroResponse = (response: ServerErrorResponse | ClientErrorResponse) => {
    if (response.type === ResponseType.SERVER_ERROR) {
        openErrorModal((<ServerErrorResponse>response).error)
    } else if(response.type === ResponseType.CLIENT_ERROR) {
        openErrorModal((<ClientErrorResponse>response).body.description)
    }
    
}

const openErrorModal = (errorMessage: string) => {
    console.log('Open Error modal')
}

Este es uno de los casos de uso más comúnes. Y aunque no es tan entretenida como el ejemplo de las armas en el videojuego probablemente te ayude a visualizar mejor en que partes de tus proyectos puedes hacer uso de ambas.