Skip to content

[dart:js_interop] Add external isA and asA helpers to dart:js_interop for is and as equivalents #54138

Closed
@srujzs

Description

@srujzs

Related issue: #52030

Users are used to using is and as for dart:html objects. This allowed them to write simple code like x is Window, and know that x actually is a JS Window object. Replicating this on some arbitrary object x (let's say it's typed as JSAny?) requires the following with dart:js_interop:

x.typeofEquals('object') && (x as JSObject).instanceOfString('Window')

which is definitely an ergonomic downgrade. Even if we move instanceOfString up to JSAny? (which may be worthwhile), it's still less ideal to have to type out the name of the type. And if we had more complex types, like user-defined JS interop types that exist in a particular library, that String gets more annoying to write.

One option is transformations to handle this. Consider a method external bool isA<T>() that exists on JSAny?. This method will do the following (assume that dart:js_interop types are extension types, which they will be):

  1. Validate that T is an extension interop type or a @staticInterop type.
  2. If T is an extension type that is one of the dart:js_interop types or any number of extension types on them, do the following:
  • If the dart:js_interop type is one of the non-JSObject types, lower to the equivalent typeofEquals check e.g. isA<JSString>() -> typeofEquals('string'). We might want to be a little careful here of extension types that may wrap these types e.g. extension type S(JSString s). I'm inclined to say that we should still lower to the same check here and ignore any renaming users may do.
  • If it is, then use the @JS() renaming annotations (including the library's) if any to determine the type users want and then do an instanceofString check using that type e.g. isA<JSArray> -> instanceofString('Array').
  1. If T is @staticInterop type, this is the same as the second bullet point of step 2.

This would make the check now look like:

x.isA<Window>()

T asA<T>() could just be implemented as:

if (!this.isA<T>) throw TypeError(...);
return this as T;

We might not want an asA because users may think they need it after an isA, which they don't. However, it is useful for users who want stronger checking and if we can mark isA as a function whose value doesn't change (handwaving dart2js optimizations here), then maybe we can optimize the duplicate isA.

Note that this doesn't give us type promotion still, but that wouldn't be possible without some language functionality anyways.

cc @kevmoo because migrating without this is admittedly less ergonomic when working with package:web types.

  • Addendum (1/4/24):

A potential wrinkle here is the treatment of types that have object literal constructors e.g.

extension type ObjectLiteral(JSObject _) {
  external ObjectLiteral({JSAny foo});
}

If we wrote isA<ObjectLiteral>(x), what are we asking for here? The above algorithm would try do a x instanceof globalContext.ObjectLiteral, but that's likely not what the user wants. We could make this check equivalent to a typeof x == 'object' to better capture the fact that we're using it as an object literal, but this might not be what the user wants either. After all, there might actually exist a globalContext.ObjectLiteral that the user wants to check here. Should we make this an error? Maybe, but that's a loss of expressivity for users.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-web-jsIssues related to JavaScript support for Dart Web, including DDC, dart2js, and JS interop.web-js-interopIssues that impact all js interop

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions