Evolución de Dart: de la flexibilidad de Dart 1 a la solidez de Dart 3

Dart es un lenguaje que ha pasado por una metamorfosis completa para pasar de ser una herramienta de scripting para la web a un lenguaje de tipado fuerte y predecible. Si lo analizas como una línea de tiempo, Dart 1 era un lenguaje de tipado opcional, muy similar a JavaScript, donde era muy fácil cometer errores que solo aparecerían cuando el programa ya estuviera corriendo. Con la llegada de Dart 2, el lenguaje dio un giro radical hacia un sistema de tipos robusto (sound type system) para garantizar la seguridad de la memoria y evitar errores de nulidad. Finalmente, Dart 3 ha refinado esto añadiendo capacidades de un lenguaje funcional moderno.

Entender esta evolución es vital para saber qué código es “seguro” y qué código es “legacy”. El sistema de tipos sound significa que el compilador y la máquina virtual (VM) garantizan que una variable siempre contendrá el tipo que dice tener; si defines un int, no hay forma de que contenga un String en tiempo de ejecución. Esto es una diferencia fundamental con TypeScript: en TypeScript, los tipos son una “capa de pintura” que desaparece al compilar, permitiendo que el código falle de forma inesperada si intentas forzar un tipo. En Dart, el contrato es inquebrantable.

Saber cuándo usar cada versión te ayuda a decidir si un proyecto necesita una migración mediante la herramienta dart migrate o si puedes aprovechar las nuevas sintaxis de Dart 3. Si configuras mal la restricción del SDK en tu pubspec.yaml (por ejemplo, poniendo sdk: '>=2.12.0 <3.0.0' pero usando funciones de Dart 3), el compilador se detendrá y te dará errores de sintaxis.

// Ejemplo que demuestra las capacidades de Dart 3 en un flujo de autenticación
sealed class AuthStatus {}

class Authenticated extends AuthStatus {
  final String user;
  Authenticated(this.user);
}

class Unauthenticated extends AuthStatus {}

class Loading extends AuthStatus {}

void main() {
  // Usamos Records (Dart 3) para retornar múltiples valores sin crear una clase extra
  (String, bool) getSession() => ("Carlos", true);

  // Pattern Matching: Desestructuramos el Record directamente
  final (userName, isLoggedIn) = getSession();

  // Creamos el estado basado en los datos obtenidos
  final AuthStatus status = isLoggedIn 
      ? Authenticated(userName) 
      : Unauthenticated();

  // Switch Expression con Pattern Matching (Dart 3)
  // El compilador sabe que AuthStatus es 'sealed', así que nos obliga a cubrir todos los casos
  final message = switch (status) {
    Authenticated(user: var name) => 'Bienvenido de nuevo, $name',
    Unauthenticated() => 'Debes iniciar sesión para continuar',
    Loading() => 'Cargando datos del servidor...',
  };

  print(message);
}

En este ejemplo, la clase AuthStatus es una sealed class [disponible desde Dart 3.0]. Esto es crucial porque le dice al compilador que el único posible valor para status es una de las tres clases que definimos (Authenticated, Unauthenticated o Loading). Gracias a esto, cuando llegamos al switch, no necesitamos un caso default. Si olvidáramos Loading, el compilador nos daría un error de inmediato.

La variable (userName, isLoggedIn) utiliza records, una forma de agrupar datos de forma anónima y ligera, evitando la verbosidad de crear una clase solo para devolver dos valores. Al asignar (userName, isLoggedIn) = getSession(), estamos usando pattern matching para extraer los valores del record directamente en variables locales.

Finalmente, dentro del switch, la línea Authenticated(user: var name) no es un simple if. Es un patrón que busca específicamente si el objeto es de tipo Authenticated y, si es así, extrae su propiedad user y la asigna a la nueva variable name. Todo esto es verificado en tiempo de compilación, lo que hace que el código sea extremadamente rápido y seguro.

El error frecuente

A veces, por venir de lenguajes con tipado débil o por intentar “solucionar” un error rápido, recurrimos al uso de dynamic. Cuando marcas algo como dynamic, le estás diciendo al compilador: “no verifiques nada, yo me encargo”.

void errorExample() {
  dynamic variable: dynamic = "Soy un String";
  
  // Esto compila perfectamente...
  print(variable.length); 

  // Pero esto fallará en tiempo de ejecución con un RuntimeError, 
  // porque intentas tratar un String como si fuera un int.
  // En un sistema de tipos sound, esto sería un error de compilación.
  int numero = variable as int; 
  print(numero + 10);
}

Si utilizas dynamic de forma excesiva, pierdes toda la ventaja de la evolución de Dart hacia la solidez del sistema de tipos, convirtiendo errores de lógica en fallos catastróficos cuando el programa ya esté en producción.

2

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio