Typescript: Uniones discriminadas o como crear argumentos opcionales y dependientes

Typescript: Uniones discriminadas o como crear argumentos opcionales y dependientes

Matías Hernández's photo
Matías Hernández
·Oct 3, 2022·

3 min read

Es común tener componentes o funciones que aceptan argumentos que dependen entre sí, dónde una puede estar presente y la otra no.

¿Sabías que con Typescript puedes asegurarte de que este comportamiento se cumpla correctamente?

Veamos una situación imaginaria usando React

Tienes un componente que representa un gato, y este gato puede estar vivo o muerto y este comportamiento o estado está definido por las props que recibe.

function Cat({ isAlive, isDead}) {
  if(isAlive != null && isDead != null) {
    throw 'Ambas props no pueden estar definidas'
  } 
  let str = isAlive ? 'vivo' : 'muerto'

  return <>Este gato esta {str}</>
}


<Cat isAlive isDead />

Pero evidentemente este gato no puede estar en ambos estados a la vez, si no, sería un gato the Schrödinger en vez de un gato real.

Además, el significado de ambas prop isAlive y isDead es similar pero contrario, por lo que para asegurar el correcto funcionamiento tendrías que escribir bloques condicionales que revisen las props y darle prioridad a una sobre otra.

¿Cómo puedes representar esto con typescript y por qué lo harías?

La idea de representar este comportamiento con Typescript es que el equívoco uso de las props sea detectado antes de que el código sea ejecutado y sin necesidad de escribir extra lógica para revisar el contenido de las props.

Para lograrlo harás uso de una combinación de propiedades opcionales, el tipo never y uniones.

/*
* Props de un gato vivo
* Si isAlive está presente entonces isDead no debe estarlo 
*/
type LivingCat = { isAlive: boolean, isDead?: never }

/*
* Props de un gato muerto
* Si isDead está presente entonces isLive no debe estarlo 
*/
type DeadCat = { isAlive?: never, isDead: boolean }

/*
* Crea una unión de las tipos definidos
*/
type RealCatProps = LivingCat | DeadCat 

/*
* Solo una prop puede estar presente. 
* La validación de este comportamiento se realiza en tiempo de compilación
*/
const RealCat = ({ isAlive, isDead}: RealCatProps) => {
    const str = isAlive ? 'living' : 'dead'
    return <>this is a {str} real cat</>
}

En éste código se definen dos tipos muy similare LivingCat y DeadCat la diferencia radica en que propiedad esta definida como never.

En el primer caso, isDead se marca como opcional y como never, es decir, nunca será utilizada o definida.

El tipo never es utilizado cuando estás seguro que "algo" jamás ocurrirá.

En el segundo caso, ocurre lo contrario. La propiedad isLiving está marcada como opcional y como never.

Finalmente, creas una unión de los tipos, creando el tipo RealCatProps y usarás dicho tipo para definir las props del component RealCat.

Este component RealCat podrá ser instanciado sólo con una de las props, pero jamás ambas.

const RealApp = () => {
    return <RealCat isAlive />
}

/*
* Esto falla en tiempo de compilación ya que ambas props están presentes
*/
const ReailFalingApp = () => {
    return <RealCat isAlive={false} isDead={false} />
}

Te invito a jugar con el código en este ejemplo con React o en el playground de typescript

 
Share this