Extension Types: Tipado nominal con costo cero en Dart

Un extension type [disponible desde Dart 3.3] es un mecanismo para crear un nuevo tipo nominal sobre un tipo existente, conocido como tipo representacional, sin incurrir en el coste de rendimiento de una clase tradicional. A diferencia de un typedef, que es simplemente un alias (un nombre distinto para el mismo tipo), un extension type crea una identidad única para el compilador. Esto permite que un UserId no sea intercambiable con un String genérico, evitando errores lógicos en tiempo de compilación. La magia reside en que, tras la compilación AOT, el “wrapper” desaparece: la VM trata el dato directamente como su tipo base, eliminando cualquier sobrecarga de memoria o ciclos de CPU para la creación de objetos. Sin embargo, si no utilizas la palabra clave implements, el nuevo tipo será estrictamente cerrado, es decir, solo tendrá acceso a los miembros que tú definas explícitamente, no a los del tipo representacional.

Si intentas resolver esto usando un typedef, el compilador permitirá mezclar tipos que deberían ser distintos, rompiendo la integridad de tu modelo de dominio. Si optas por una class tradicional para ganar seguridad de tipos, estarás pagando un coste de memoria por cada instancia, ya que cada objeto tendrá su propio encabezado en el heap.

// Definimos un UserId que se comporta como un String para el resto del mundo
// pero es un tipo distinto para el compilador.
extension type UserId(String value) implements String {
  bool get isInternal => value.startsWith('int_');
}

// Definimos un Email que NO implementa String.
// Esto significa que NO podemos usar .contains() o .toLowerCase() 
// a menos que lo definamos explícitamente.
extension type Email(String value) {
  bool get isValid => value.contains('@');
  
  // Delegamos explícitamente un método si lo necesitamos
  String get rawValue => value;
}

/// Una función que exige tipos estrictos para evitar confusiones.
void procesarUsuario(UserId id, Email email) {
  if (!email.isValid) {
    throw Exception('Email inválido: ${email.rawValue}');
  }

  // Al implementar String, UserId puede usar métodos de String directamente.
  print('Procesando ID: ${id.toUpperCase()} para el correo ${email.rawValue}');
  
  if (id.isInternal) {
    print('Atención: El usuario tiene un ID interno.');
  }
}

void main() {
  // Instanciación instantánea, sin asignación de objeto en el heap.
  final id = UserId('int_998877');
  final email = Email('dev@ejemplo.com');

  procesarUsuario(id, email);

  // CASO DE USO: Interop y seguridad
  // El siguiente código daría error de compilación:
  // procesarUsuario(email, id); 
  
  // El siguiente código también daría error (un String no es un UserId):
  // procesarUsuario('not-an-id', email);
}

En el ejemplo anterior, hemos creado dos wrappers con comportamientos distintos. UserId utiliza implements String, lo que significa que el compilador le otorga automáticamente todos los métodos de la interfaz String (como toUpperCase()). Esto es extremadamente útil cuando quieres añadir lógica de negocio (como el getter isInternal) sin perder la capacidad de usar la API nativa de la clase original. Por el contrario, Email no utiliza implements. Esto encapsula el tipo de forma total: aunque por dentro es un String, no puedes llamar a .toLowerCase() sobre un objeto Email directamente; tienes que acceder a rawValue.

Cuando ejecutas este código, el compilador de Dart realiza una optimización agresiva. En el bytecode final, id y email no son objetos complejos; son simplemente los valores String subyacentes. No hay “boxing” (envoltorio de objeto), lo que significa que el rendimiento es idéntico al de trabajar con strings primitivos, pero la seguridad de tipos es la de un sistema nominal estricto.

El error frecuente

Un error común es confundir la necesidad de un extension type con la de un typedef cuando se busca seguridad de tipos.

// MAL: Uso de typedef (Alias transparente)
typedef UserId = String;
typedef Email = String;

void procesar(UserId id, Email email) {}

void main() {
  const id = "user_1";
  const email = "test@test.com";

  // Esto compila sin errores, pero es un error lógico grave:
  // Estamos pasando un Email donde se espera un UserId.
  procesar(email, id); 
}

Si usas typedef, el compilador trata a UserId y Email como el mismo tipo (String). El error de lógica solo se detectará en tiempo de ejecución (o peor, no se detectará) en lugar de en tiempo de compilación. Usa extension type siempre que quieras que el compilador te impida confundir dos valores que, aunque sean del mismo tipo base, representan conceptos distintos en tu lógica de negocio.

60

Dejar un comentario

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

Scroll al inicio