Las uniones discriminantes en TypeScript son una técnica muy útil para manejar objetos que pueden tener diferentes formas, pero que comparten un campo común que nos permite distinguir entre ellos. Este campo, que tiene el mismo nombre en todos los tipos que forman la unión, contiene un valor literal único para cada subtipo, lo que nos ayuda a identificar con seguridad qué tipo de objeto estamos tratando en cada momento.
Para entenderlo mejor, imaginemos que definimos distintos tipos para representar operaciones matemáticas. Por ejemplo, una operación suma podría ser un objeto con dos propiedades, sumando1 y sumando2, y un campo especial llamado tipo cuyo valor literal es "suma". Esto significa que para que un objeto sea considerado una operación suma, su propiedad tipo debe tener exactamente el valor "suma". De manera similar, podemos definir una operación multiplicar con propiedades operando1 y operando2, y un campo tipo con el valor literal "multiplicar".
Con estos tipos definidos, podemos crear una unión llamada Operacion que sea la combinación de OperacionSuma y OperacionMultiplicar. Así, una variable de tipo Operacion puede contener cualquiera de estas dos formas.
Cuando escribimos una función que recibe una Operacion, por ejemplo operar, nos encontramos con que TypeScript solo nos ofrece acceso a las propiedades comunes a todos los tipos de la unión, como el campo tipo. Sin embargo, gracias a que tipo es un literal que solo puede ser "suma" o "multiplicar", podemos usarlo como una guarda para diferenciar qué tipo de operación tenemos en cada caso.
Por ejemplo, si dentro de la función comprobamos que o.tipo === "suma", TypeScript infiere automáticamente que o es de tipo OperacionSuma. Esto nos permite acceder sin problemas a las propiedades sumando1 y sumando2. De forma análoga, si o.tipo === "multiplicar", TypeScript sabe que o es de tipo OperacionMultiplicar y nos deja trabajar con operando1 y operando2.
Este patrón es especialmente valioso porque nos permite escribir código seguro y claro, evitando errores al acceder a propiedades que no existen en ciertos tipos. Además, es un enfoque que se utiliza mucho en librerías populares. Por ejemplo, Redux usa un campo type en sus acciones para distinguir entre diferentes tipos de acciones, y Discord.js emplea un sistema similar para diferenciar entre mensajes enviados a canales, grupos de DM o mensajes directos a una persona.
En definitiva, las uniones discriminantes nos permiten construir jerarquías de tipos complejas y trabajar con ellas de manera sencilla y segura, aprovechando el poder del sistema de tipos de TypeScript para discriminar entre variantes de objetos según un valor literal común. Esto hace que sea una herramienta fundamental para cualquier desarrollador que quiera manejar tipos de forma avanzada y mantener un código robusto y mantenible.
Para ilustrar este concepto, podemos definir los tipos y la función operar de la siguiente manera:
type OperacionSuma = {
tipo: "suma";
sumando1: number;
sumando2: number;
};
type OperacionMultiplicar = {
tipo: "multiplicar";
operando1: number;
operando2: number;
};
type Operacion = OperacionSuma | OperacionMultiplicar;
function operar(o: Operacion): number {
if (o.tipo === "suma") {
return o.sumando1 + o.sumando2;
} else if (o.tipo === "multiplicar") {
return o.operando1 * o.operando2;
}
// Opcionalmente, podríamos manejar un caso por defecto o lanzar un error
}
Con este código, podemos pasar a la función operar cualquier objeto que sea una suma o una multiplicación, y gracias a la unión discriminante, TypeScript nos ayuda a trabajar con cada tipo de forma segura y con autocompletado, mejorando la experiencia de desarrollo y la calidad del código.