Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a dart:js_interop API that can determine if an Object is a JS value #56905

Open
srujzs opened this issue Oct 16, 2024 · 15 comments
Open

Add a dart:js_interop API that can determine if an Object is a JS value #56905

srujzs opened this issue Oct 16, 2024 · 15 comments
Assignees
Labels
area-web Use area-web for Dart web related issues, including the DDC and dart2js compilers and JS interop. web-js-interop Issues that impact all js interop

Comments

@srujzs
Copy link
Contributor

srujzs commented Oct 16, 2024

Historically, it was common for users to treat JS values through interop as Dart types and not JS types, mainly because the concept of "JS types" did not exist yet. So, values may flow through as Object or dynamic to other parts of code. This code may accept both a Dart and a JS value, making it difficult to determine how to treat such a value without some way of distinguishing the two runtimes.

The canonical way to type-check Dart objects is to use is, but we don't want users using that if it's a JS value, as it may give inconsistent values. This is partially why we have the invalid_runtime_check_with_js_interop_types lint.

The canonical way to type-check JS values is to use isA, but that requires a cast to JSAny in the first place. This cast will always succeed when compiling to JS (because it's Object under-the-hood there), but may fail when compiling to Wasm (because it's JSValue). This is our best option today.

So, to determine how to type-check a given Object, it is useful to know whether it's a JS value in the first place. One such mechanism is a compiler-specific external patched API in dart:js_interop.

Some quirks that we might come across:

  • Primitive types (String, int/double/num, bool) are both Dart and JS values when compiling to JS. This is not true when compiling to Wasm. We may want to always treat them as JS values for the sake of this API, but users should be aware that they should still type-check for the Dart primitive types when this API returns false so that they handle the Wasm case correctly.
  • Dart functions are JS functions in DDC, so the implementation should use the type system internals to differentiate them instead of interop.
  • Inconsistencies in our internal type system checks. Hopefully this should be fine in most realistic use cases, but we've seen edge-cases before e.g. DDC treating prototypes of compiled Dart objects as Dart objects whereas dart2js and dart2wasm do not. I expect dart2wasm should generally have no issues with this check due to the boxing.
  • We may want to modify the invalid_runtime_check_with_js_interop_types check to account for this new API e.g. pointing users to this API when they do a is JSAny check.
@srujzs srujzs added web-js-interop Issues that impact all js interop area-web Use area-web for Dart web related issues, including the DDC and dart2js compilers and JS interop. labels Oct 16, 2024
@srujzs srujzs changed the title Add a dart:js_interop API that can determine if a Object is a JS value Add a dart:js_interop API that can determine if an Object is a JS value Oct 16, 2024
@srujzs srujzs self-assigned this Jan 18, 2025
@srujzs srujzs marked this as a duplicate of #59932 Jan 18, 2025
@gmpassos
Copy link
Contributor

gmpassos commented Jan 18, 2025

Any update on this?

@gmpassos
Copy link
Contributor

FYI: @kevmoo @mit-mit

We urgently need something like the extension below, with official support for each platform:

extension ObjectToJSAnyNullableExtension on Object? {
  bool get isJSAny;
  bool get isJSObject;
}

NOTE: After discussing this issue with other developers, we concluded that, without a secure way to cast Object to JSAny, the new dart:js_interop is not stable for real-world use cases.

@kevmoo
Copy link
Member

kevmoo commented Jan 20, 2025

@srujzs is the expert here. I will make sure we follow-up this week.

@gmpassos
Copy link
Contributor

@srujzs is the expert here. I will make sure we follow-up this week.

Thanks for the quick response

@srujzs
Copy link
Contributor Author

srujzs commented Jan 22, 2025

I want to get this done before the next stable, for both the JS compilers and dart2wasm.

I don't believe we need isJSObject as long as we can check if something is a JS value. This would then allow you to cast to JSAny? and safely type-check via isA<JSObject>.

@gmpassos
Copy link
Contributor

I agree that isJSObject is not mandatory. However, since most cases of using JSAny.isA<T>() involve a T that is not a primitive, isJSObject can be very useful.

It also provides an opportunity for an optimized implementation of isJSObject. Additionally, it is easier to ensure that an Object is a JSObject, as the primitive implementations of JSAny can be ambiguous depending on the platform.

Therefore, it is much easier to implement isJSObject in cases where isJSAny becomes ambiguous.

I have internally implemented a bool? isJSAny that returns null in cases where the detection is ambiguous. However, for isJSObject, there are no ambiguous cases.

@gmpassos
Copy link
Contributor

FYI: @srujzs

I have published the package js_interop_utils, which includes a temporary implementation of isJSAny and isJSObject until an official Dart implementation becomes available.

bool? get isJSAny {
https://github.com/gmpassos/js_interop_utils/blob/39682fa7dc11e91b03a5390882f4538e304e21c7/lib/src/js_interop_utils_extensions.dart#L9

bool? get isJSObject {
https://github.com/gmpassos/js_interop_utils/blob/39682fa7dc11e91b03a5390882f4538e304e21c7/lib/src/js_interop_utils_extensions.dart#L99

This might help clarify the issues involved in implementing an official version.

@lrhn
Copy link
Member

lrhn commented Jan 22, 2025

What is the intended use-case for this API?

When compiled to JavaScript, all values are JavaScript values, so it sounds like the function of really asking isNotDartValue if it plans to return false for any value, and it will need done easy to recognize which non.-primitive values are actually Dart values.

On Wasm, there are Dart types representing and containing JS values, so it can easily check if a value is one of those.

Is that the intended behavior?

Can you trust that if isJSValue returns true, you can safely cast to JSValue? (And then maybe call toDart?)

@gmpassos
Copy link
Contributor

When working on a real-world project, it’s very common to have a DOM element passed through an Object variable or parameter.

Once you have an Object variable and want to check, for example, if it’s an HTMLElement or a JSObject of any other class declared using an extension type, you need to use JSAny?.isA<T>().

Without a safe way to cast an Object back to a JSAny, it’s impossible to work with a JS value safely. Not to mention that the Dart code never knows if it’s running in JS or Wasm "mode."

@gmpassos
Copy link
Contributor

@srujzs
Copy link
Contributor Author

srujzs commented Jan 23, 2025

I can see how, from an implementation perspective, isJSObject may be faster if you need to do all the primitive checks for isJSAny. We may be able to circumvent that by checking if a value is a Dart object, and if it's not, return true. But still, point considered that isJSObject may be desired for a faster type-check.

re: your implementations, returning null when it's ambiguous is interesting, but I'm not sure it changes the user's action much. They'd still need to check specifically what type it is if it's ambiguous. If we only returned true or false, this would be the same. Maybe the set of types they need to check is smaller when it's ambiguous, but that's an implementation detail they shouldn't know about. I expect the real platform-specific implementation to avoid a lot of the checks in this file, but that's of course due to us being able to use compiler intrinsics.

Is that the intended behavior? Can you trust that if isJSValue returns true, you can safely cast to JSValue? (And then maybe call toDart?)

This is more or less what I intend for this s/JSValue/JSAny. If yes, cast to JSAny, and do the type-checks you need with isA. If false, do the type checks you need with is. This is to simply recover the static JS type once it's been lost due to an upcast to Object.

Object o;
if (o.isJSAny) {
  if (o.isA<JSString>()) {
    ...
  } else if (o.isA<JSNumber>()) {
    ...
  }
  ...
} else {
  if (o is String) {
    ...
  } else if (o is num) {
    ...
  }
}

In this case, the isA checks in the first branch are both the ambiguous case and the JS value case, but that seems okay to me. I'm not sure why you would want to operate differently based on how the type system could interpret it rather than on the value itself, but I'm open to an example!

@gmpassos
Copy link
Contributor

gmpassos commented Jan 23, 2025

In this case, the isA checks in the first branch are both the ambiguous case and the JS value case.

I wasn’t expecting to have ambiguous cases with “true”, as all primitives will be treated as JS values depending on the platform. It can be complicated to include all String, num, and bool types as JSAny, if they can be treated as normal Dart types.

I vote for “true” to not include ambiguous cases. Do not forget that the user, as a developer, will write !isJSAny and isJSAny if you don’t return null for ambiguous states. In other words, if you can only trust “true,” then !"false" does not have the same meaning, as false can also represent ambiguity.

IMHO, if ambiguous states are possible, the function can only have two implementations:

  • isJSAny: returns true, false, or null (ambiguous).

  • isJSAny: returns true or null (ambiguous or false).

You cannot return a false that may also indicate an ambiguous state, as this would lead to misuse of the function.

But yes, an isXYZ getter that can return null is an unusual choice for an API.

@gmpassos
Copy link
Contributor

gmpassos commented Jan 23, 2025

Note: I was hoping that we wouldn’t have ambiguous cases/detections in the official implementation. However, due to the internal implementation for JS or Wasm, it might be necessary.

This is why isJSObject is also interesting: it doesn’t have ambiguous detections, so we can always trust its return value.

@lrhn
Copy link
Member

lrhn commented Jan 23, 2025

I'd go for returning true if as JSAny will succeed, and the object is not a plain Dart object, and false otherwise.

It does mean that primitives in JS code will return true, but likely instances of Dart classes, records and maybe functions will be recognized. It's not wrong, primitives can be cast to JSAny.

On Warm it's just is _JSAny, or whatever the top of the JS value hierarchy is called.

There are objects that are unambiguously Dart values, and objects that are definitely not, and then there might be values which can be either, and the only answer that is useful there is whether as JSAny will work, which means true.

Telling people that it could be either isn't helping them.

@gmpassos
Copy link
Contributor

gmpassos commented Jan 23, 2025

I'd go for returning true if as JSAny will succeed, and the object is not a plain Dart object, and false otherwise.
...
There are objects that are unambiguously Dart values, and objects that are definitely not, and then there might be values which can be either, and the only answer that is useful there is whether as JSAny will work, which means true.

Telling people that it could be either isn't helping them.

I agree with you. After thinking more about it, a core function that returns true|false|null is not a good idea. Developers are not accustomed to handling this kind of pattern and may use it incorrectly.

I think a good solution is to have isJSAny return true when as JSAny is safe. However, we also need isJSObject to avoid isJSAny -> true for primitives when we only want to check for a class type. If a developer wants to check for primitives and JSObject, they can use isJSAny (which will return many true values depending on the platform).

With isJSAny, we can ensure safety, and with isJSObject, we can ensure performance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-web Use area-web for Dart web related issues, including the DDC and dart2js compilers and JS interop. web-js-interop Issues that impact all js interop
Projects
Status: No status
Development

No branches or pull requests

4 participants