La comunicación entre Dart y el ecosistema JavaScript ha evolucionado drásticamente. Si vienes de usar dart:js o package:js, notarás que el enfoque ha cambiado: ya no nos basamos en objetos dynamic con validación en tiempo de ejecución, sino en tipos opacos que garantizan la compatibilidad con la compilación AOT y, fundamentalmente, con WasmGC [disponible desde Dart 3.0].
Para lograr esto, dart:js_interop utiliza tipos como JSObject, JSString, JSNumber y JSPromise<T>. Estos no son clases de Dart convencionales con lógica interna; son representaciones de bajo nivel que el compilador reconoce para mapear directamente al valor de JavaScript subyacente. Esto elimina la sobrecarga de tener que “traducir” constantemente objetos complejos entre los dos mundos, permitiendo que la comunicación sea casi tan rápida como si fuera código nativo.
Para usar esto, debes entender cuándo necesitas la seguridad de tipos y cuándo necesitas la flexibilidad de dart:js_interop_unsafe. Si intentas acceder a una propiedad de un objeto JS que no ha sido declarada mediante una extension type o un binding @JS, el compilador te protegerá, pero si fuerzas la mano con unsafe y el tipo no coincide, el error ocurrirá en el runtime de JS, lo que puede ser difícil de rastrear.
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
// Definimos la interfaz de una API externa de JavaScript.
// Usamos @JS para mapear funciones globales de la ventana o módulos.
@JS('console.log')
external void jsLog(JSString message);
@JS('fetch')
external JSPromise<JSObject> fetch(JSString url);
// Las 'extension types' [disponible desde Dart 3.3] son la clave de la interop moderna.
// Actúan como un envoltorio (wrapper) sobre un JSObject, permitiéndonos
// tener tipado fuerte en Dart sin el coste de runtime de una clase normal.
@JS()
extension type UserProfile(JSObject _) implements JSObject {
external String get name;
external int get age;
}
void main() async {
// 1. Conversión de tipos primitivos (Dart -> JS)
// No es un cast, es una conversión explícita para crear el wrapper opaco.
const nameStr = 'Ingeniero Senior'.toJS;
const ageVal = 35.toJS;
jsLog("Configurando perfil...".toJS);
// 2. Creación de objetos JS desde Dart
// Creamos un objeto JS vacío y lo tratamos como nuestro tipo UserProfile.
final user = JSObject() as UserProfile;
user.setProperty('name'.toJS, nameStr);
user.setProperty('age'.toJS, ageVal);
print('Perfil en Dart: ${user.name} (${user.age} años)');
// 3. Interacción con promesas y asincronía (JS Promise -> Dart Future)
// JSPromise<T> se convierte en Future<T> mediante el método .toDart.
print('Iniciando petición de red...');
try {
final promise = fetch('https://api.github.com/users/octocat'.toJS);
// .toDart conecta el event loop de JS con el microtask queue de Dart.
final response = await promise.toDart;
print('Respuesta recibida: $response');
} catch (e) {
print('Error en la petición: $e');
}
// 4. Acceso dinámico con dart:js_interop_unsafe
// Si recibes un objeto de una librería NPM que no puedes mapear con extension types,
// usamos métodos unsafe para leer propiedades dinámicamente.
final data = JSObject();
data.setProperty('metadata'.toJS, 'v1.2.3'.toJS);
final metadata = data.getProperty('metadata'.toJS);
if (metadata != null) {
print('Metadata extraída dinámicamente: ${metadata.toDart}');
}
}
Desglose del código
En el ejemplo anterior, observa cómo UserProfile no es una clase con campos reales, sino una extension type que implementa JSObject. Esto significa que, en el código compilado a JavaScript, user es simplemente un objeto literal {name: "...", age: ...}, pero para el compilador de Dart, tiene propiedades con tipos específicos.
Cuando ejecutamos user.name, Dart no busca una propiedad en una clase de Dart; llama directamente a la propiedad del objeto JavaScript. El método .toJS sobre el string "Configurando perfil..." es lo que transforma el String de Dart (que tiene su propia estructura de memoria) en un JSString que el motor de JavaScript puede entender.
La parte más crítica para la arquitectura es el manejo de la asincronía. Un JSPromise<JSObject> no es un Future. Son mecanismos de ejecución distintos. El método .toDart es el puente que permite que el await de Dart suspenda la ejecución de la corrutina hasta que la promesa de JavaScript se resuelva, integrando ambos sistemas de eventos de forma transparente.
Finalmente, el uso de setProperty y getProperty del paquete unsafe es la última línea de defensa. Estos métodos trabajan directamente sobre la memoria del objeto JS mediante punteros/referencias, permitiendo interactuar con cualquier estructura de datos de JavaScript sin necesidad de definiciones previas, aunque a costa de perder la seguridad que ofrece el sistema de tipos de Dart.
El error frecuente
Un error clásico es intentar usar .toDart sobre un objeto que no contiene el tipo que esperas. Por ejemplo:
// Si el JS devuelve un objeto y no un String final jsValue = someJSFunction().toDart; // Si esperabas un String pero llegó un JSObject print(jsValue.length); // Error de runtime: jsValue es un objeto, no un String.
Dado que toDart en tipos como JSString o JSNumber es esencialmente un “unwrapping” de un valor opaco, si el objeto subyacente en JavaScript no tiene el tipo correcto, el error no aparecerá al compilar, sino cuando intentes acceder a las propiedades del valor extraído en tiempo de ejecución. Siempre valida el tipo o usa js_interop_unsafe para inspeccionar la estructura si el origen de los datos no es controlado.
N° 111