Una sealed class [disponible desde Dart 3.0] es una forma de declarar un tipo cuyos subtipos son conocidos y finitos en tiempo de compilación. A diferencia de las abstract class tradicionales, donde cualquier parte de tu proyecto puede extender una clase, las clases selladas restringen la herencia exclusivamente a las clases definidas en el mismo archivo (o librería). Esto transforma al compilador en un validador de lógica de negocio: al conocer todos los posibles subtipos, el runtime no tiene que adivinar qué tipo de objeto está recibiendo, y el compilador puede garantizar la exhaustividad en las operaciones de control.
Este mecanismo permite implementar lo que en otros lenguajes funcionales se conocen como Tipos de Datos Algebraicos (ADTs), específicamente sum types. Debes usar sealed cuando tengas un conjunto de estados o tipos que no deben crecer arbitrariamente, como los estados de una conexión (conectado, desconectando, desconectado, error) o los resultados de una operación (éxito, fallo). Si intentas usar un switch sobre un tipo sellado y olvidas cubrir uno de sus subtipos, el compilador lanzará un error de inmediato, impidiendo que el código llegue a producción con un comportamiento errático o un error de “non-exhaustive match”.
// El archivo debe contener la jerarquía completa
sealed class MetodoPago {
final double monto;
const MetodoPago(this.monto);
}
class Tarjeta extends MetodoPago {
final String numero;
final String cvv;
Tarjeta(double monto, this.numero, this.cvv) : super(monto);
}
class PayPal extends MetodoPago {
final String email;
PayPal(double monto, this.email) : super(monto);
}
class Efectivo extends MetodoPago {
final String local;
Efectivo(double monto, this.local) : super(monto);
}
/// Procesa el pago utilizando una switch expression.
String procesar(MetodoPago pago) {
// El compilador verifica que todos los subtipos de MetodoPago estén cubiertos.
return switch (pago) {
Tarjeta(numero: var n, cvv: var c) => 'Pagando \$${pago.monto} con tarjeta terminada en ${n.substring(n.length - 4)} (CVV: $c)',
PayPal(email: var e) => 'Pagando \$${pago.monto} mediante cuenta PayPal: $e',
Efectivo(local: var l) => 'Cobrando \$${pago.monto} en efectivo en $l',
};
}
void main() {
final misPagos = [
Tarjeta(150.50, '1234567812345678', '123'),
PayPal(45.0, 'usuario@ejemplo.com'),
Efectivo(10.0, 'Cafetería Central'),
];
for (final pago in misPagos) {
print(procesar(pago));
}
}
En el ejemplo anterior, MetodoPago es nuestra clase raíz. Al ser sealed, el compilador sabe que no existen otros tipos de pago más allá de Tarjeta, PayPal y Efectivo dentro de este contexto.
Cuando llamamos a procesar(MetodoPago pago), estamos usando una switch expression. Fíjate en la sintaxis de Tarjeta(numero: var n, cvv: var c). Aquí estamos aplicando pattern matching (patrones de coincidencia): no solo estamos comprobando si el objeto es una Tarjeta, sino que estamos extrayendo sus propiedades numero y cvv directamente en variables locales n y c para usarlas en la rama correspondiente.
Si intentaras agregar una nueva clase Cripto extends MetodoPago en el mismo archivo, el compilador de Dart te señalaría inmediatamente la función procesar indicando que el switch ya no es exhaustivo. Esto es lo que hace que las sealed classes sean superiores a los enums para representar modelos de datos complejos: mientras que un enum solo puede representar valores constantes, cada subclase de una sealed class puede tener sus propios campos, tipos y estructuras de datos (como el email en PayPal frente al numero en Tarjeta).
El error frecuente ocurre cuando intentas usar una sealed class como si fuera una abstract class abierta para el desarrollo de librerías. Si defines una sealed class en un paquete y un usuario de ese paquete intenta extenderla en su propio archivo para añadir un nuevo tipo de pago, el compilador fallará. Si necesitas permitir que otros desarrolladores extiendan tu jerarquía de tipos, no uses sealed, usa abstract.
N° 91