La switch expression [disponible desde Dart 3.0] representa un cambio de paradigma respecto al switch statement tradicional. Mientras que el switch clásico es una estructura de control imperativa diseñada para ejecutar bloques de código y efectos secundarios, la expresión es una entidad funcional que devuelve un valor directamente. En términos de diseño, esto permite tratar la lógica de decisión como una asignación de datos, facilitando la inmutabilidad y reduciendo drásticamente el ruido visual de los condicionales anidados.
Al usar una expresión, el compilador de Dart aplica un análisis de flujo mucho más agresivo. Si aplicas una switch expression sobre un tipo sealed o un enum, el compilador garantiza la exhaustividad (exhaustiveness). Esto significa que si intentas evaluar un tipo que tiene sub-tipos conocidos y no cubres todos los casos, el compilador lanzará un error en tiempo de compilación. Esta característica es la que hace que el sistema de tipos de Dart sea “sound” (robusto) al trabajar con sumas de tipos: si añades un nuevo estado a una clase sealed, todos los switch expressions que la utilicen en tu proyecto dejarán de compilar hasta que manejes el nuevo caso, evitando errores silenciosos en producción.
Sin embargo, esta potencia conlleva un riesgo: el uso del comodín _ (wildcard). Si utilizas _ para “parchar” la exhaustividad en una clase sealed, estarás anulando la protección del compilador. El código compilará, pero el sistema perderá la capacidad de avisarte cuando la estructura de tus datos cambie, delegando esa responsabilidad al programador en lugar de al lenguaje.
Para aprovechar la expresividad, debemos usarla cuando la lógica de decisión dependa de la estructura del objeto, no solo de su valor. Es ideal para transformar estados complejos en valores simples, sustituyendo los ternarios anidados que son casi ilegibles.
sealed class OrderStatus {}
class Pending implements OrderStatus {}
class Processing implements OrderStatus {
final int itemsCount;
final double totalValue;
Processing(this.itemsCount, this.totalValue);
}
class Shipped implements OrderStatus {
final String trackingCode;
final DateTime estimatedArrival;
Shipped(this.trackingCode, this.estimatedArrival);
}
class Cancelled implements OrderStatus {
final String reason;
Cancelled(this.reason);
}
void main() {
final List<OrderStatus> orders = [
Pending(),
Processing(3, 150.50),
Processing(1, 45.00),
Shipped('ABC-123', DateTime.now().add(const Duration(days: 2))),
Cancelled('Out of stock'),
];
final List<String> labels = orders.map((status) => getOrderLabel(status)).toList();
for (var label in labels) {
print(label);
}
}
String getOrderLabel(OrderStatus status) {
// La expresión devuelve un String. El tipo de retorno es inferido
// como el "least upper bound" de todos los valores devueltos.
return switch (status) {
Pending() => 'Esperando confirmación',
// Destructuring de propiedades y guardas (when)
Processing(itemsCount: var count, totalValue: var total)
when total > 100 => 'Procesando pedido premium ($count artículos)',
Processing(_, var total) => 'Procesando pedido estándar: \$$total',
// Pattern matching con records y lógica OR (||)
// Imaginemos que Shipped tiene una propiedad que evaluamos en un record
Shipped(trackingCode: var code, estimatedArrival: var date)
when date.isBefore(DateTime.now().add(const Duration(days: 5)))
=> 'Envío rápido: $code',
Shipped(var code, _) when code.startsWith('EXP') => 'Envío Express: $code',
Shipped(var code, _) => 'Envío estándar: $code',
// Combinación de patrones con el operador OR
Cancelled(reason: var r) when r == 'Out of stock' || r == 'Payment failed'
=> 'Cancelado por defecto: $r',
Cancelled(var reason) => 'Cancelado por: $reason',
// El comodín _ es el último recurso
_ => 'Estado desconocido',
};
}
Análisis técnico del ejemplo
En getOrderLabel, la lógica no solo compara valores, sino que realiza destructuring de los objetos. En la línea Processing(itemsCount: var count, totalValue: var total), el motor de Dart extrae las propiedades directamente al scope de esa rama de la expresión. Fíjate en la diferencia entre la primera y la segunda rama de Processing: la primera utiliza una guard (when) para filtrar pedidos de alto valor, mientras que la segunda usa el comodín _ para capturar cualquier otro caso de Processing ignorando el conteo de artículos pero capturando el totalValue.
El compilador realiza un análisis de tipos sobre el switch (status). Dado que OrderStatus es una sealed class, el compilador sabe exactamente qué subclases existen. Si elimináramos la rama Cancelled, el compilador de Dart 3 nos daría un error de exhaustividad. Sin embargo, al incluir _ => ... al final, le estamos diciendo al compilador: “No te preocupes por la exhaustividad, yo me encargo de capturar lo que sobre”. Esto es útil para tipos abiertos, pero en sealed classes es una mala práctica que rompe la seguridad del sistema.
Además, observa cómo la expresión maneja la lógica combinada. El uso de || en el patrón de Cancelled permite agrupar condiciones lógicas dentro de la propia estructura de coincidencia, lo que es mucho más eficiente y legible que encadenar múltiples if o case en un switch tradicional. El tipo de retorno de la expresión se infiere automáticamente como String, ya que todas las ramas, incluso las que usan when o _, terminan devolviendo un valor de ese tipo.
El error frecuente
Un error sutil ocurre al intentar usar patrones de desestructuración con tipos que no son sealed o enum. Si intentas hacer un switch sobre una clase normal (no sealed) y olvidas el caso por defecto _, el compilador no te avisará de que falta un caso.
// ERROR COMÚN
class NormalClass {
final int id;
NormalClass(this.id);
}
String errorPráctico(NormalClass obj) => switch (obj) {
// El compilador NO dará error aquí, pero fallará en runtime
// si recibe un tipo que no encaje con el patrón esperado
// (aunque en este caso sea imposible, la idea es que en
// jerarquías abiertas la exhaustividad es ilusoria).
NormalClass(id: var i) when i > 0 => 'Positivo',
};
Si la clase no es sealed, la expresión switch requiere que cubras todas las posibilidades de forma explícita o mediante un _. El peligro real es cuando mezclas la comodidad de la switch expression con la falta de rigor de las clases no selladas, perdiendo la principal ventaja de Dart 3: la seguridad de que tu lógica de negocio es total.
N° 87