El if-case [disponible desde Dart 3.0] es una sentencia que combina la evaluación de una condición lógica con la desestructuración de un objeto. En lugar de realizar una comprobación de tipo con is y luego realizar un cast manual con as (un proceso propenso a errores y verboso), el if-case intenta que el valor encaje en un patrón específico. Si el valor coincide con la forma del patrón, este no solo devuelve true, sino que “extrae” los datos relevantes y los vincula a nuevas variables (proceso conocido como variable binding).
La razón de su existencia es la eficiencia tanto en la escritura como en la ejecución. Internamente, la VM de Dart optimiza estas comprobaciones para que la validación del tipo y la extracción de campos ocurran en un solo paso, evitando la redundancia de inspeccionar el objeto múltiples veces. Debes usarlo siempre que necesites extraer información de una estructura compleja (como un Map de un JSON o un Record) de forma segura y rápida. Si intentas usar las variables vinculadas fuera del bloque if donde fueron definidas, o si intentas acceder a ellas en un bloque else (donde el patrón falló y no hubo vinculación), el compilador te detendrá, ya que su scope (alcance) está estrictamente limitado al bloque donde la coincidencia fue exitosa.
void main() {
// Simulamos datos recibidos de una API
final Map<String, dynamic> response = {
'status': 200,
'data': {
'user_id': 42,
'username': 'dart_dev',
'roles': ['admin', 'editor'],
}
};
final Map<String, dynamic> errorResponse = {
'status': 404,
'message': 'Not Found',
};
// 1. Extracción profunda y tipada de un Map
// Buscamos que 'data' sea un Map y que contenga las llaves con sus tipos correctos.
if (response case {'data': Map<String, dynamic> data, 'status': 200}) {
// Aquí 'data' está vinculada y ya sabemos que el status es 200
print('Conexión exitosa. Procesando datos...');
// Podemos anidar patrones para extraer valores de niveles profundos de una vez
if (data case {'user_id': int id, 'username': String name}) {
// 'id' y 'name' se extraen directamente sin casts manuales
print('Usuario detectado: $name (ID: $id)');
}
}
// 2. Uso de 'else if' para encadenamiento de patrones
// Evaluamos diferentes formas en las que la respuesta puede venir
if (errorResponse case {'status': int code, 'message': String msg}) {
print('Error de API: Código $code - $msg');
} else if (response case {'status': int code, 'data': Map _}) {
// Usamos '_' (wildcard) si solo nos importa que la llave exista pero no su valor
print('Respuesta con datos, pero sin detalle de error específico.');
}
// 3. Comparación con el enfoque tradicional (Lo que NO deberías hacer)
final Map<String, dynamic> rawData = {'id': 10};
// En Dart 2.x / antes de Dart 3.0:
if (rawData is Map && rawData['id'] is int) {
final int id = rawData['id'] as int; // Cast manual tedioso y peligroso
print('ID antiguo: $id');
}
}
En el ejemplo anterior, observa cómo en la primera sentencia if (response case {'data': Map<String, dynamic> data, 'status': 200}), estamos realizando una validación múltiple. No solo verificamos que response sea un Map, sino que validamos que la llave status tenga exactamente el valor 200 y que data exista y sea un Map. Si todo esto es cierto, la variable data queda disponible para usarse dentro de las llaves del bloque if.
Cuando profundizamos con if (data case {'user_id': int id, 'username': String name}), el motor de Dart realiza la desestructuración de las propiedades del mapa directamente. Fíjate que no hay un as int ni un as String; el patrón int id actúa como un filtro y un asignador simultáneamente. Si user_id no fuera un int (por ejemplo, si fuera un String), el patrón fallaría, la condición resultaría en false y el flujo saltaría al siguiente else if o al final, manteniendo la integridad de los tipos.
El uso del guion bajo _ en {'data': Map _} es fundamental cuando quieres verificar la estructura de un objeto pero no tienes intención de utilizar su contenido. Esto le indica al compilador que no necesita reservar memoria para una variable ni realizar un binding, lo cual es una buena práctica de rendimiento cuando la lógica solo depende de la existencia de la llave.
El error frecuente
Un error común al usar patrones es asumir que las variables extraídas tienen un alcance global dentro del método. El compilador de Dart aplica un análisis de flujo de control muy estricto.
void errorExample(Map<String, dynamic> data) {
if (data case {'name': String name}) {
print('Hola $name');
// 'name' es perfectamente válida aquí.
} else {
// ERROR: 'name' no es accesible aquí.
// El patrón no coincidió, por lo tanto, la variable 'name' nunca se creó.
// print(name);
}
// ERROR: 'name' es inaccesible aquí.
// El alcance de la variable vinculada termina exactamente donde termina el bloque 'if'.
// print(name);
}
Si intentas usar name fuera del bloque if, recibirás un error de compilación: The getter 'name' isn't defined for the type 'void'. Si necesitas usar ese valor más allá, debes declararlo antes del if o utilizar una estructura de control que maneje el flujo de manera distinta, como un switch o asignar el valor a una variable externa dentro del bloque.
N° 88