ts-pattern es una librería de TypeScript para hacer pattern matching, que es una técnica para comparar estructuras de datos de manera elegante y segura. Facilita el manejo de datos complejos, eliminando muchos if y switch anidados.

Con ts-pattern puedes:
✅ Escribir código más limpio y seguro.
✅ Evitar errores al olvidarte de manejar un caso específico.
✅ Tener mejor inferencia de tipos en TypeScript.

Instalación

Para usar ts-pattern, primero instálalo en tu proyecto:

npm install ts-pattern

También puedes usar yarn o pnpm:

yarn add ts-pattern
pnpm add ts-pattern

🟨 Ejemplo Básico: Reemplazando un switch

Digamos que tenemos un estado en una aplicación:

type Estado = "cargando" | "exito" | "error";

const estado: Estado = "exito";
switch (estado) {
  case "cargando":
    console.log("Cargando datos...");
    break;
  case "exito":
    console.log("¡Datos cargados con éxito!");
    break;
  case "error":
    console.log("Hubo un error 😢");
    break;
}

Con ts-pattern, el código es más limpio y seguro:

import { match } from "ts-pattern";

const mensaje = match(estado)
  .with("cargando", () => "Cargando datos...")
  .with("exito", () => "¡Datos cargados con éxito!")
  .with("error", () => "Hubo un error 😢")
  .exhaustive(); // Asegura que cubrimos todos los casos

console.log(mensaje);

¿Qué hace este código?
match(estado) compara el valor de estado con diferentes patrones ("cargando", "exito", "error") y ejecuta la función correspondiente.

✅ Ventaja: Si olvidamos manejar un caso, ts-pattern nos avisa en tiempo de compilación.

🟨 Ejemplo con Objetos: Manejo de Errores

Imagina que estás manejando respuestas de una API con diferentes formatos:

type Respuesta =
  | { tipo: "ok"; data: string }
  | { tipo: "error"; mensaje: string };

const respuesta: Respuesta = { tipo: "error", mensaje: "No encontrado" };

Con if haríamos algo así:

if (respuesta.tipo === "ok") {
  console.log(`Datos: ${respuesta.data}`);
} else {
  console.log(`Error: ${respuesta.mensaje}`);
}

Con ts-pattern, el código es más elegante:

const resultado = match(respuesta)
  .with({ tipo: "ok" }, (r) => `Datos: ${r.data}`)
  .with({ tipo: "error" }, (r) => `Error: ${r.mensaje}`)
  .exhaustive();

console.log(resultado);

¿Qué hace este código?
Compara respuesta con los patrones { tipo: "ok" } y { tipo: "error" }.
Accede a r.data o r.mensaje automáticamente sin necesidad de hacer un if.

✅ Ventaja: TypeScript infiere el tipo correctamente sin necesidad de hacer as o comprobaciones manuales.

Uso con Expresiones Regulares o Rango de Valores

También puedes hacer matching con números, rangos o expresiones regulares. Por ejemplo, clasificamos edades:

const edad = 25;

const categoria = match(edad)
  .when((e) => e < 18, () => "Menor de edad")
  .when((e) => e >= 18 && e < 65, () => "Adulto")
  .when((e) => e >= 65, () => "Tercera edad")
  .otherwise(() => "Desconocido");

console.log(categoria); // "Adulto"

¿Qué hace este código?
Usa .when() para definir condiciones personalizadas.
Usa .otherwise() como caso por defecto.

✅ Ventaja: Es más declarativo que usar múltiples if.

🟨 Tipar match

Se puede tipar de forma segura gracias a TypeScript. Normalmente, TypeScript infiere los tipos automáticamente, pero si queremos asegurarnos de que solo acepte ciertos valores, podemos hacerlo manualmente.

Hay dos formas principales de tiparlo:

1) ** Tipado automático (sin definir el tipo)**
Cuando pasamos un valor a match, TypeScript ya infiere el tipo:

import { match } from "ts-pattern";

const estado = "cargando" as "cargando" | "exito" | "error";

const mensaje = match(estado)
  .with("cargando", () => "Cargando datos...")
  .with("exito", () => "¡Datos cargados con éxito!")
  .with("error", () => "Hubo un error 😢")
  .exhaustive();

console.log(mensaje);

¿Qué pasa aquí?
Como estado está tipado como "cargando" | "exito" | "error", TypeScript ya valida que match solo acepte esos valores.
Si olvidamos un caso, TypeScript dará error con .exhaustive().
✅ Ventaja: No necesitamos definir el tipo manualmente.

2) Tipado explícito (definiendo el tipo)
Si queremos ser más estrictos, podemos tipar manualmente match con match():

type Estado = "cargando" | "exito" | "error";

const estado: Estado = "exito";

const mensaje = match<Estado>(estado) // 👈 Aquí forzamos el tipo
  .with("cargando", () => "Cargando datos...")
  .with("exito", () => "¡Datos cargados con éxito!")
  .with("error", () => "Hubo un error 😢")
  .exhaustive();

console.log(mensaje);

✅ Ventaja: Si estado no tiene el tipo correcto, TypeScript mostrará error.

3) Extra: Tipado en Objetos
Cuando usamos objetos, también podemos tipar match para asegurar que todas las propiedades sean correctas.

Ejemplo con un objeto Respuesta:

type Respuesta =
  | { tipo: "ok"; data: string }
  | { tipo: "error"; mensaje: string };

const respuesta: Respuesta = { tipo: "ok", data: "Hola" };

const resultado = match<Respuesta>(respuesta) // 👈 Tipamos aquí
  .with({ tipo: "ok" }, (r) => `Datos: ${r.data}`)
  .with({ tipo: "error" }, (r) => `Error: ${r.mensaje}`)
  .exhaustive();

console.log(resultado);

✅ Ventaja:

  • TypeScript detecta automáticamente si olvidamos manejar un tipo de Respuesta.
  • r dentro de cada .with() ya tiene el tipo correcto (r.data o r.mensaje).

¿Necesito tipar match?

  • No siempre. TypeScript ya infiere los tipos en la mayoría de los casos.
  • Si queremos asegurarnos, podemos usar match(). Lo mejor: .exhaustive() siempre verifica que no falte ningún caso.