Algebraic Data Types en TS: Flujos de pago irrompibles
Si el flujo de pago tiene un campo status: string, estás a un typo de cobrar dos veces; un "pednign" silencioso en producción no tira error: tira plata.
Los Algebraic Data Types (ADTs) en TypeScript permiten modelar estados donde lo inválido es directamente ilegal a nivel del compilador. No “imposible”. Ilegal.
El problema: la bolsa de opcionales#
// ❌ Todo es posible... incluyendo cobrar dos veces
interface PaymentState {
status: string;
transactionId?: string;
errorMessage?: string;
receiptUrl?: string;
retryable?: boolean;
}
// ¿Cuándo existe transactionId?
// ¿Cuándo es seguro leer receiptUrl?
// Nadie sabe. Runtime lo dirá.// ✅ El compilador prohíbe estados ilegales
type PaymentState =
| { status: "idle" }
| { status: "pending"; intentId: string }
| {
status: "processing";
intentId: string;
gateway: "bancard" | "stripe";
}
| {
status: "success";
transactionId: string;
receiptUrl: string;
}
| {
status: "failed";
errorCode: number;
errorMessage: string;
retryable: boolean;
};Cada variante del union lleva solo los datos que le corresponden. No hay transactionId en "pending", no hay errorMessage en "success". El compilador lo garantiza.
El guardia: exhaustive switch con never#
function handlePayment(state: PaymentState): string {
switch (state.status) {
case "idle":
return "Esperando acción del usuario";
case "pending":
return `Intent creado: ${state.intentId}`;
case "processing":
return `Procesando en ${state.gateway}...`;
case "success":
return `✅ TX: ${state.transactionId}`;
case "failed":
return state.retryable
? `Error ${state.errorCode}: reintentar`
: `Error fatal: ${state.errorMessage}`;
default:
// Agregás un estado nuevo y no lo manejás?
// TypeScript grita acá ↓
const _exhaustive: never = state;
return _exhaustive;
}
}¿Por qué no string enums o clases?#
ADTs vs alternativas
Discriminated Unions (ADTs)
- Datos específicos por estado (no opcionales sueltos)
- Narrowing automático del compilador en cada case
- Exhaustiveness check nativo con never
- Zero overhead en runtime (se borra en compilación)
String Enums / Class Hierarchy
- Enums: no vinculan datos al estado, todo sigue opcional
- Clases: overhead de runtime + instanceof frágil
- Enums + interfaces: duplicás la fuente de verdad
- Falsa sensación de seguridad si usás type-casting para 'arreglar' el narrowing
Por qué funciona (El Aha! Moment)#
Un ADT combina estados finitos con datos específicos por variante. El compilador es tu primer QA. Si olvidás un caso, grita antes de que el código llegue a staging. En pagos no alcanza con que un estado sea “improbable”; necesitás que sea directamente ilegal de representar.
Un string es una promesa. Un union es un contrato.
Si querés complementar esto con validación en runtime, mirá cómo usar .superRefine() en Zod 4 para validación cross-field. Y para ver cómo Astro 6 valida colecciones con estos mismos principios, empezá por la migración a Zod 4 en el Content Layer.