Saltar al contenido principal

Tipos de JS

Información de uso sobre los tipos principales en JS interop.

Los valores de Dart y los valores de JS pertenecen a dominios de lenguaje separados. Al compilar a Wasm, también se ejecutan en entornos de ejecución (runtimes) separados. Por lo tanto, debes tratar los valores de JS como tipos externos. Para proporcionar tipos de Dart para los valores de JS, dart:js_interop expone un conjunto de tipos con el prefijo JS llamados "tipos de JS". Estos tipos se utilizan para distinguir entre valores de Dart y valores de JS en tiempo de compilación.

Es importante destacar que estos tipos se materializan (reified) de manera diferente según se compile a Wasm o a JS. Esto significa que su tipo en tiempo de ejecución diferirá y, por lo tanto, no puedes usar comprobaciones is ni conversiones de tipo (casts) as. Para interactuar con estos valores de JS y examinarlos, debes usar miembros de interoperabilidad external o conversiones.

Jerarquía de tipos

#

Los tipos de JS forman una jerarquía de tipos natural:

  • Top type: JSAny, which is any non-nullish JS value
    • Primitivos: JSNumber, JSBoolean, JSString, JSSymbol, JSBigInt
    • JSObject, which is any JS object
      • JSFunction
        • JSExportedDartFunction, que representa un callback de Dart que se convirtió en una función de JS
      • JSArray
      • JSPromise
      • JSDataView
      • JSTypedArray
        • Arrays tipados de JS como JSUint8Array
      • JSBoxedDartObject, which allows users to box and pass Dart values opaquely within the same Dart runtime
        • Desde Dart 3.4 en adelante, el tipo ExternalDartReference en dart:js_interop también permite a los usuarios pasar valores de Dart de forma opaca, pero no es un tipo de JS. Obtén más información sobre las ventajas y desventajas de cada opción aquí.

Puedes encontrar la definición de cada tipo en la documentación de la API de dart:js_interop.

Conversiones

#

Para usar un valor de un dominio a otro, probablemente querrás convertir el valor al tipo correspondiente del otro dominio. Por ejemplo, podrías querer convertir una List<JSString> de Dart en un array de cadenas de JS, lo cual está representado por el tipo de JS JSArray<JSString>, para que puedas pasar el array a una API de JS interop.

Dart proporciona una serie de miembros de conversión en varios tipos de Dart y tipos de JS para realizar la conversión de los valores entre dominios por ti.

Los miembros que convierten valores de Dart a JS suelen comenzar con toJS:

dart
String str = 'hello world';
JSString jsStr = str.toJS;

Los miembros que convierten valores de JS a Dart suelen comenzar con toDart:

dart
JSNumber jsNum = ...;
int integer = jsNum.toDartInt;

No todos los tipos de JS tienen una conversión, y no todos los tipos de Dart tienen una conversión. Generalmente, la tabla de conversión se ve como la siguiente:

Tipo de dart:js_interopTipo de Dart
JSNumber, JSBoolean, JSString num, int, double, bool, String
JSExportedDartFunctionFunction
JSArray<T extends JSAny?> List<T extends JSAny?>
JSPromise<T extends JSAny?> Future<T extends JSAny?>
Arrays tipados como JSUint8Array Listas tipadas de dart:typed_data
JSBoxedDartObjectValor de Dart opaco
ExternalDartReferenceValor de Dart opaco

Requisitos en declaraciones external y Function.toJS

#

Para garantizar la seguridad de tipos y la consistencia, el compilador impone requisitos sobre qué tipos pueden fluir hacia y desde JS. No se permite pasar valores arbitrarios de Dart a JS. En su lugar, el compilador requiere que los usuarios utilicen un tipo de interoperabilidad compatible, ExternalDartReference, o un primitivo, que luego sería convertido implícitamente por el compilador. Por ejemplo, se permitiría lo siguiente:

bien dart
@JS()
external void primitives(String a, int b, double c, num d, bool e);
bien dart
@JS()
external JSArray jsTypes(JSObject _, JSString __);
bien dart
extension type InteropType(JSObject _) implements JSObject {}

@JS()
external InteropType get interopType;
bien dart
@JS()
external void externalDartReference(ExternalDartReference _);

Mientras que estos devolverían un error:

mal dart
@JS()
external Function get function;
mal dart
@JS()
external set list(List _);

Estos mismos requisitos existen cuando utilizas Function.toJS para hacer que una función de Dart sea invocable en JS. Los valores que fluyen hacia y desde este callback deben ser un tipo de interoperabilidad compatible o un primitivo.

Si usas un primitivo de Dart como String, se produce una conversión implícita en el compilador para convertir ese valor de un valor de JS a un valor de Dart. Si el rendimiento es crítico y no necesitas examinar el contenido de la cadena, entonces usar JSString en su lugar para evitar el costo de la conversión puede tener sentido, como en el segundo ejemplo.

Compatibilidad, comprobaciones de tipo y conversiones de tipo (casts)

#

El tipo en tiempo de ejecución de los tipos de JS puede diferir según el compilador. Esto afecta las comprobaciones de tipo en tiempo de ejecución y las conversiones de tipo (casts). Por lo tanto, casi siempre evita las comprobaciones is donde el valor es un tipo de interoperabilidad o donde el tipo de destino es un tipo de interoperabilidad:

mal dart
void f(JSAny a) {
  if (a is String) {  }
}
mal dart
void f(JSAny a) {
  if (a is JSObject) {  }
}

Además, evita las conversiones de tipo (casts) entre tipos de Dart y tipos de interoperabilidad:

mal dart
void f(JSString s) {
  s as String;
}

Para comprobar el tipo de un valor de JS, usa un miembro de interoperabilidad como typeofEquals o instanceOfString que examine el propio valor de JS:

bien dart
void f(JSAny a) {
  // Here `a` is verified to be a JS function, so the cast is okay.
  if (a.typeofEquals('function')) {
    a as JSFunction;
  }
}

Desde Dart 3.4 en adelante, puedes usar la función auxiliar isA para comprobar si un valor es cualquier tipo de interoperabilidad:

bien dart
void f(JSAny a) {
  if (a.isA<JSString>()) {} // `typeofEquals('string')`
  if (a.isA<JSArray>()) {} // `instanceOfString('Array')`
  if (a.isA<CustomInteropType>()) {} // `instanceOfString('CustomInteropType')`
}

Dependiendo del parámetro de tipo, transformará la llamada en la comprobación de tipo adecuada para ese tipo.

Para evitar comprobaciones de tipo no válidas con tipos de JS interop, habilita la regla de linter invalid_runtime_check_with_js_interop_types.

null vs undefined

#

JS tiene tanto un valor null como un valor undefined. Esto contrasta con Dart, que solo tiene null. Con el fin de hacer que los valores de JS sean más ergonómicos de usar, si un miembro de interoperabilidad devolviera un valor null o undefined de JS, el compilador mapea estos valores al null de Dart. Por lo tanto, un miembro como value en el siguiente ejemplo puede interpretarse como que devuelve un objeto JS, null de JS o undefined:

dart
@JS()
external JSObject? get value;

Si el tipo de retorno no se declaró como anulable, el programa lanzará un error si el valor devuelto fuera null o undefined de JS para garantizar la solidez (soundness).

JSBoxedDartObject vs ExternalDartReference

#

Desde Dart 3.4 en adelante, tanto JSBoxedDartObject como ExternalDartReference pueden usarse para pasar referencias opacas a Objects de Dart a través de JavaScript. Sin embargo, JSBoxedDartObject envuelve la referencia opaca en un objeto de JavaScript, mientras que ExternalDartReference es la referencia en sí y, por lo tanto, no es un tipo de JS.

Usa JSBoxedDartObject si necesitas un tipo de JS o si necesitas comprobaciones adicionales para asegurarte de que los valores de Dart no se pasen a otro entorno de ejecución (runtime) de Dart. Por ejemplo, si el objeto de Dart debe colocarse en un JSArray o pasarse a una API que acepta un JSAny, usa JSBoxedDartObject. Usa ExternalDartReference en caso contrario, ya que será más rápido.

Consulta toExternalReference y toDartObject para realizar la conversión hacia y desde una ExternalDartReference.