JsInterop

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against GWT 2.13.1 (2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, jsinterop-annotations 2.0.0 (the version the GWT BOM pins), jsinterop-base 1.2.0 and Elemental2 1.3.2, and the Jakarta Servlet 6 server of the Bookshelf scenario, checked on 2026-10-08. JsInterop is client knowledge: the annotations are read by the GWT compiler while it translates client and shared Java to JavaScript, and they decide what the generated JavaScript looks like at the boundary with the browser, with third-party scripts and with the host page. The build side is one compiler flag and one module inheritance, both covered below; there is nothing to do on the server. Every example on this page was compiled with the 2.13.1 compiler (from the command line and through mvn package with the gwt-app packaging), loaded in headless Chrome and exercised from JavaScript; the outputs and compiler messages are quoted as they were printed.

JsInterop is the modern way for Java and JavaScript to talk to each other in GWT. It replaced hand-written JavaScript inside Java comments (JSNI, covered in JSNI & Overlay Types) with annotations on ordinary Java declarations: a Java type that stands for something JavaScript already provides is a native type, and a Java type that JavaScript is meant to call is an exported type. The Elemental2 browser bindings (see DOM & Elemental2), jsinterop-base and parts of GWT’s emulated java.util classes are written with these annotations, so the rules on this page are the rules of the whole ecosystem.

The two directions

There are exactly two directions, and each has its own kind of Java type.

flowchart LR subgraph java["Java compiled by GWT"] app["Bookshelf presenters
and widgets"] native["Native types
JsType(isNative = true)
NumberFormatJs, ChartJs, Shelf"] exp["Exported types
JsType
bookshelf.Api, bookshelf.BookRef"] cb["JsFunction interfaces
Listener, ClickHandler"] end subgraph js["JavaScript in the page"] browser["Browser globals
Intl, JSON, fetch, Promise"] lib["Third-party scripts
chart library, legacy Shelf"] host["Host page and other scripts
bookshelf.Api.open(7)"] end app -- "calls" --> native native -- "plain JS call or property access" --> browser native -- "plain JS call or property access" --> lib host -- "needs -generateJsInteropExports" --> exp exp -- "ordinary Java method call" --> app lib -- "invokes a JS function" --> cb cb -- "lambda body runs as Java" --> app app -- "passes a lambda" --> cb
  • Java calls JavaScript. You write a Java declaration with the shape of the JavaScript API, mark it @JsType(isNative = true), and the compiler emits a direct access to the JavaScript name. There is no wrapper object and no glue code: new NumberFormatJs("de-DE") compiles to new $wnd.Intl.NumberFormat("de-DE").

  • JavaScript calls Java. You mark a Java type or member as exported and (in GWT) compile with -generateJsInteropExports; the compiler then makes the member reachable under a stable, non-obfuscated JavaScript name such as bookshelf.Api.formatCopies.

  • Callbacks cross both ways through @JsFunction interfaces: a Java lambda becomes a real JavaScript function, and a JavaScript function becomes a Java interface instance.

The two kinds of type are easy to confuse because they use the same annotation, so keep this table in mind.

Table 1. Native types versus exported types
Native type: @JsType(isNative = true) Exported type: @JsType

Who implements it

JavaScript. The Java class is only a description (a facade).

Java. JavaScript only calls it.

Method bodies

native or abstract only (a @JsOverlay method may have a body).

Ordinary Java.

Names in the output

The JavaScript name given by namespace and name, never obfuscated.

namespace (default: the Java package) and name (default: the simple class name), never obfuscated, but only emitted when the exports flag is on.

instanceof

Allowed against native classes, rejected against native interfaces.

Ordinary Java instanceof.

Typical size cost

Zero: nothing is generated for the facade.

Every exported member is kept alive, so it cannot be pruned.

Setting up

JsInterop needs three things in a Maven build: the annotations jar (already on the classpath), the compiler flag for exports (only if JavaScript calls Java), and optionally the helper libraries.

Dependencies added to a GWT module for this page (bookshelf-interop-lab/pom.xml)
<dependencies>
  <dependency>
    <groupId>org.gwtproject</groupId>
    <artifactId>gwt-user</artifactId>
  </dependency>
  <dependency>
    <groupId>org.gwtproject</groupId>
    <artifactId>gwt-dev</artifactId>
  </dependency>
  <dependency>
    <groupId>com.google.jsinterop</groupId>
    <artifactId>base</artifactId>
    <version>1.2.0</version>
  </dependency>
  <dependency>
    <groupId>com.google.elemental2</groupId>
    <artifactId>elemental2-dom</artifactId>
    <version>1.3.2</version>
  </dependency>
  <dependency>
    <groupId>com.google.elemental2</groupId>
    <artifactId>elemental2-promise</artifactId>
    <version>1.3.2</version>
  </dependency>
</dependencies>

The org.gwtproject:gwt BOM manages jsinterop-annotations (2.0.0 in the 2.13.1 BOM), and gwt-user brings it in transitively, so the annotations need no dependency of their own. mvn dependency:tree on the module above printed com.google.jsinterop:jsinterop-annotations:jar:2.0.0:compile under gwt-user, com.google.jsinterop:base:jar:1.2.0:compile with org.jspecify:jspecify:jar:0.3.0:compile, and the three Elemental2 jars (elemental2-dom, elemental2-core and elemental2-promise, all 1.3.2). Maven Central also lists jsinterop-annotations 2.1.0 (2023-07-18); jsinterop-base 1.2.0 itself declares 2.0.2, and the BOM-managed 2.0.0 won the resolution without any problem.

Each library that has GWT-specific source needs a <inherits> line in the module XML, because GWT compiles from source, not from bytecode.

Module XML of the lab (src/main/module.gwt.xml)
<?xml version="1.0" encoding="UTF-8"?>
<module>
  <inherits name="com.google.gwt.user.User"/>
  <inherits name="elemental2.dom.Dom"/>
  <inherits name="elemental2.promise.Promise"/>
  <inherits name="jsinterop.base.Base"/>
  <source path="client"/>
  <entry-point class="com.example.lab.client.Lab"/>
</module>

The annotations themselves need no <inherits>; jsinterop.base.Base is the module named in the jsinterop-base README, and the two Elemental2 modules above come from the .gwt.xml files in the 1.3.2 jars (elemental2/dom/Dom.gwt.xml, elemental2/promise/Promise.gwt.xml, which itself inherits jsinterop.base.Base).

The exports flag goes to the production compiler and to Super Dev Mode, and the Maven plugin has a separate argument list for each of them (see Build Tools: Maven & Gradle for the plugin). The flag is described among the compiler flags in Compiler & Compilation, and Super Dev Mode in Development Mode & Debugging.

The exports flag for both the production compile and Super Dev Mode
<plugin>
  <groupId>net.ltgt.gwt.maven</groupId>
  <artifactId>gwt-maven-plugin</artifactId>
  <configuration>
    <moduleName>com.example.lab.Lab</moduleName>
    <moduleShortName>lab</moduleShortName>
    <compilerArgs>
      <arg>-generateJsInteropExports</arg>
    </compilerArgs>
    <codeserverArgs>
      <arg>-generateJsInteropExports</arg>
    </codeserverArgs>
  </configuration>
</plugin>

The Bookshelf scratch application sets only compilerArgs; that is enough for mvn package, but gwt:codeserver reads codeserverArgs, so a developer who tests bookshelf.Api from the host page in Super Dev Mode must add the second list. The CodeServer class accepts the flag itself: started on the command line with -generateJsInteropExports, its compiled output contained 'bookshelf.Api' and 'bookshelf.BookRef' registrations, and without the flag the same compile contained none.

The Developer Guide’s own link for the annotation Javadoc, /javadoc/latest/jsinterop/annotations/package-summary.html on gwtproject.org, answered 404 on 2026-10-08, and the DevGuideJsInterop.html name some pages use is also a 404; the real page is DevGuideCodingBasicsJsInterop.html (linked from the Client-side menu as "JavaScript: JsInterop"). The annotation Javadoc is on javadoc.io.

The annotations

The table below was built from the jsinterop-annotations 2.0.0 sources (the @Target of each annotation) and from the 2.13.1 compiler sources (JsInteropUtil reads the annotations; JsInteropRestrictionChecker enforces the rules), and every row marked run was compiled and run in the lab.

Table 2. The jsinterop-annotations 2.0.0 annotations and what GWT 2.13.1 does with them
Annotation Applies to (@Target) Meaning GWT 2.13.1

@JsType

a type

Attributes name, namespace (both default to <auto>) and isNative (default false). Without isNative it exports all public members; with it, the type describes a JavaScript type.

Honoured (run).

@JsMethod

a method

Attributes name and namespace. Renames a method or gives a static method a JavaScript namespace. Needed when the Java name cannot be the JavaScript name, as in @JsMethod(name = "catch").

Honoured (run).

@JsProperty

a method or a field

Attributes name and namespace. On a field it names the property; on a method it marks a getter or setter whose name follows the JavaBean convention, unless name is given.

Honoured (run).

@JsConstructor

a constructor

The constructor that JavaScript calls with new. A class has at most one, and every other constructor must delegate to it.

Honoured (run).

@JsFunction

a type

Marks a functional interface (exactly one abstract method) as the Java view of a JavaScript function.

Honoured (run).

@JsOverlay

a method or a field

Adds a Java-only member (a final or static method, or a static field) to a native type or a @JsFunction interface.

Honoured (run).

@JsPackage

a package

One attribute, namespace, plus the constant JsPackage.GLOBAL (the string <global>) used as the namespace of a native type that lives directly on the global object.

JsPackage.GLOBAL honoured (run). A package-level @JsPackage(namespace = ...) is read by JsInteropUtil but was not exercised here.

@JsIgnore

a constructor, a method or a field

Removes one member from the exports of a @JsType.

Honoured (run). Rejected on members of a native type.

@JsOptional

a parameter

The parameter may be omitted by JavaScript callers. It cannot be primitive and cannot precede a required parameter. Its Javadoc calls it informational for the GWT compiler.

Accepted and validated (run): the three rules are compile errors (see the pitfalls table), and an omitted String argument arrived as null.

@JsNullable, @JsNonNull

a type use

Mark a type as nullable (@JsNullable, on type variables) or non-nullable (@JsNonNull). Their Javadoc calls them informational in the GWT compiler, for other tools that annotate the output.

Accepted, no effect (run): nothing in dev/core/src of GWT 2.13.1 reads them.

@JsAsync

a method

Transpiles the method to an async JavaScript function; its Javadoc calls it experimental and says it only works in J2CL.

Accepted, no effect (run). The 2.9.0 release notes list @JsAsync and @JsEnum as the exceptions to GWT 2 / J2CL compatibility.

@JsEnum

a type

Attributes name, namespace, isNative and hasCustomValue; exports a Java enum as a JavaScript enum (J2CL).

Accepted, no effect (run): the enum stays a normal Java enum object. The GWT 2.14 milestone has an open item to explore @JsEnum support (gwtproject/gwt#9794).

The three "no effect" rows deserve a concrete picture. A probe module compiled @JsNullable, @JsNonNull, @JsAsync and an @JsEnum enum without any error. The only output was two warnings about the enum, [unusable-by-js] Type of parameter 'c' in 'String Probe.color(Probe.Color)' is not usable by but exposed to JavaScript. and the matching return-type warning, and in the browser the value returned by probe.Probe.green() was a plain Java enum object whose own keys were ["name_0","ordinal_0"]. Write these annotations only for code that must also compile with J2CL, and do not expect them to change what GWT generates.

The attribute defaults and the rules that the compiler checks are summarised here; each rule is quoted in the pitfalls table with the exact message.

  • Namespace and name of an exported type. The default namespace is the Java package (the compiler’s own message about two exports clashing names 'com.example.bad2.client.TwoCtors'), and the default name is the simple class name. JsPackage.GLOBAL as the namespace of a native type means "a global".

  • Members. A public member of a @JsType is exported with its Java name unless @JsMethod, @JsProperty or @JsIgnore says otherwise. The Developer Guide says the annotation exposes the public non-static members; on 2.13.1 the public static members of bookshelf.Api (VERSION, formatCopies, sum) were exported too.

  • Not transitive. @JsType on a class does not make the types in its signatures exportable; each needs its own annotation, or JavaScript receives opaque objects (the [unusable-by-js] warning below).

  • Unknown shapes. A native type may use the name "?" (unknown) or "*" (any): jsinterop-base names its Any type "*" and Elemental2’s union helper types use "?". The Developer Guide describes "?" for a JavaScript API that has no well-defined type, and the 2.8.1 release notes added both names.

Calling JavaScript from Java: native types

A native type is a Java class or interface with the shape of a JavaScript API. The compiler never generates its implementation; it generates calls to the JavaScript name. The rules are those of the Developer Guide and the checker in the compiler: constructors are empty, methods are native (or @JsOverlay), fields have no initializer and are not final, and a native class extends only native classes.

A first binding: Intl.NumberFormat

The Bookshelf shows the number of copies per book and the price in the reader’s locale. The browser already has a locale-aware formatter, Intl.NumberFormat, so there is nothing to ship: the binding is a facade. This is the class that the Bookshelf scratch application contains.

The Bookshelf’s binding for Intl.NumberFormat (bookshelf-client)
package com.example.bookshelf.client.interop;

import jsinterop.annotations.JsType;

/** Binding for the browser's {@code Intl.NumberFormat}; native types need no JavaScript glue. */
@JsType(isNative = true, namespace = "Intl", name = "NumberFormat")
public class NumberFormatJs {
  public NumberFormatJs(String locale) {}

  public native String format(double value);
}
  • namespace = "Intl" and name = "NumberFormat" are the JavaScript name; the Java class is called NumberFormatJs only to avoid a clash with GWT’s own com.google.gwt.i18n.client.NumberFormat.

  • The constructor body is empty, as the compiler requires; the arguments are passed straight through.

  • format(double) is native: there is no Java body, and the call compiles to .format(n) on the JavaScript object.

Compiled with -style PRETTY, a method that used the class produced this JavaScript (the whole facade disappears):

Generated JavaScript for new NumberFormatJs(locale).format(n)
return (new $wnd.Intl.NumberFormat(locale)).format(n);

$wnd is the window that hosts the page, not the iframe in which the compiled module runs. Native types with JsPackage.GLOBAL (or a namespace such as Intl) are resolved through $wnd, which is why a library loaded by an ordinary <script> tag of the host page is visible to them.

Options objects: a native type that is a plain object

JavaScript APIs take "options bags". A native type named Object in the global namespace is the idiom for them: it adds no behaviour, only typed property names.

A typed options bag with a @JsOverlay factory (FormatOptions.java)
package com.example.lab.client.intl;

import jsinterop.annotations.JsOverlay;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;

/** Options bag for {@code Intl.NumberFormat}: a plain JavaScript object, never a Java object. */
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class FormatOptions {
  public String style;
  public String currency;
  public int maximumFractionDigits;

  @JsOverlay
  public static FormatOptions currency(String code) {
    FormatOptions o = new FormatOptions();
    o.style = "currency";
    o.currency = code;
    return o;
  }
}

Two details matter here. First, the fields are public and not initialised, so only the properties that Java sets exist on the JavaScript object: the compiler printed (o = new $wnd.Object , o.style = 'currency' , o.currency = 'EUR' , o), and no maximumFractionDigits property was created. Second, @JsOverlay lets the facade carry a final helper (the factory) that is plain Java and is inlined at each call site; JavaScript never sees it. A second constructor taking the options object is a second declaration of the same native constructor.

Using the facade
NumberFormatJs eur = new NumberFormatJs("es-ES", FormatOptions.currency("EUR"));
String text = eur.format(1234.5);   // "1234,50", a no-break space and the euro sign in headless Chrome

In headless Chrome the call printed 1234,50 € for es-ES (Spanish formatting does not group four-digit numbers) and 1.234,5 for de-DE through the exported bookshelf.Api.formatCopies. The actual strings are the browser’s: they depend on its ICU data, so tests should not hard-code them.

Properties, methods and static members

JavaScript properties are not methods. Java has no way to say "this getter reads a property", so JsInterop uses an annotation on the accessor. The lab binds a small legacy script that defines a constructor Shelf with a read-only count accessor property, a label data property, a put method and a static kind property.

The legacy script (host page)
function Shelf(name) { this.name = name; this._items = []; this.label = name; }
Shelf.kind = "legacy";
Object.defineProperty(Shelf.prototype, "count", { get: function () { return this._items.length; } });
Shelf.prototype.put = function (t) { this._items.push(t); return this; };
The binding (Shelf.java)
package com.example.lab.client.shelf;

import jsinterop.annotations.JsMethod;
import jsinterop.annotations.JsOverlay;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsProperty;
import jsinterop.annotations.JsType;

/** Binding for a hypothetical legacy script: function Shelf(name) { ... }. */
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Shelf")
public class Shelf {
  @JsProperty(name = "kind")
  public static native String getKind();

  public Shelf(String name) {}

  @JsProperty
  public native int getCount();

  @JsProperty(name = "label")
  public native String getLabel();

  @JsProperty(name = "label")
  public native void setLabel(String label);

  @JsMethod(name = "put")
  public native Shelf add(String title);

  @JsOverlay
  public final Shelf addAll(String... titles) {
    for (String t : titles) {
      add(t);
    }
    return this;
  }
}
Using it, and what the browser printed
Shelf shelf = new Shelf("Fiction");
shelf.addAll("Dune", "Emma").setLabel("Shelf A");
String summary = shelf.getLabel() + ":" + shelf.getCount() + ":" + Shelf.getKind();
// summary is "Shelf A:2:legacy"
  • @JsProperty on getCount() makes the call shelf.getCount() read shelf.count; the name is derived from the JavaBean convention (getCount is property count), and @JsProperty(name = "label") gives the getter and setter an explicit name.

  • A static property accessor is allowed only on a native type, which is why getKind() reads Shelf.kind; the compiler rejects it elsewhere (Static property accessor '%s' can only be native.).

  • @JsMethod(name = "put") binds the Java method add to the JavaScript put; without it Java add would look for shelf.add.

  • addAll(String...) is a @JsOverlay method, so it exists only in Java and expands into put calls; the generated JavaScript contained no addAll.

  • A native interface describes a structural type whose implementers are plain JavaScript objects; because there is no runtime marker to test, the compiler forbids instanceof against it.

Globals, duck typing and JSON

A global function or object needs no class of its own to be wrapped. The Developer Guide’s example binds JSON, whose members are static; the same class serves the lab’s JSON examples.

A global object as a native class (JsonNative.java)
package com.example.lab.client;

import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "JSON")
public class JsonNative {
  public static native Object parse(String text);

  public static native String stringify(Object value);
}

For structures that have no constructor to name (an object returned by JSON.parse, a configuration literal), the options-bag idiom above works, and jsinterop-base (see [jsinterop-base]) offers JsPropertyMap for the dynamic case. A data transfer object that mirrors a JSON document is therefore a native type named Object:

A JSON-shaped DTO as a native type (BookDto.java)
package com.example.lab.client.exported;

import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;

/** A JSON-shaped DTO: native type means "a plain JavaScript object with these properties". */
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class BookDto {
  public String title;
  public int copies;
}

JSON.stringify of such an object printed {"title":"Dune","copies":3}. JSON.stringify of an ordinary (non-@JsType) Java object holding the same values printed {}, because the fields of a GWT-compiled Java object are not JSON-visible properties; if JavaScript, or a library such as a chart or a store, must read the fields, the object must be a native DTO or an exported type.

Callbacks: @JsFunction

A JavaScript function that is passed as an argument is a Java functional interface annotated with @JsFunction. Because the annotation implies "this is a function value", GWT turns a Java lambda that implements the interface into a real JavaScript function.

A callback type (nested in ChartOptions.java)
package com.example.lab.client.chart;

import jsinterop.annotations.JsFunction;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class ChartOptions {
  /** A JavaScript callback: the browser calls it, Java supplies it as a lambda. */
  @JsFunction
  public interface ClickHandler {
    void onClick(Object event, ChartElement[] elements);
  }

  public ClickHandler onClick;
}

A @JsFunction interface cannot extend another interface, cannot declare members other than @JsOverlay ones, and cannot be implemented by a class that is also a @JsType; each rule has a compiler message in the pitfalls table. An instanceof or cast against a @JsFunction is, per its Javadoc, a JavaScript typeof test for a function. The direction of the call decides which side owns the function: a lambda passed to a native API (options.onClick = ...) is a Java-implemented function called by JavaScript, and the parameter of an exported method typed with a @JsFunction interface (Api.subscribe(Listener)) is a JavaScript function that Java calls.

Worked example: a chart library for the Bookshelf

The Bookshelf’s admin area shows how many loans were made each month. Chart.js draws a bar chart from a configuration object: new Chart(canvas, config), a data object with labels and datasets, a chart.update() after the data is changed, chart.destroy() when the canvas is thrown away, and an onClick option that is called with the event, the active elements and the chart. That is the surface the lab binds; it is documented on the Chart.js site (links below).

The lab does not download or run Chart.js. It runs a short stand-in script with the same constructor, update, destroy and onClick surface, which records its calls; what is exercised is the binding, and a real Chart.js build would be loaded by the same <script> tag. The binding is five native types and one widget.

The constructor and the lifecycle methods (ChartJs.java)
package com.example.lab.client.chart;

import com.google.gwt.dom.client.CanvasElement;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;

/** Binding for a Chart.js-style constructor: {@code new Chart(canvas, config)}. */
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Chart")
public class ChartJs {
  public ChartData data;

  public ChartJs(CanvasElement canvas, ChartConfig config) {}

  public native void update();

  public native void destroy();
}
The configuration object and its parts (ChartConfig.java, ChartData.java, Dataset.java, ChartElement.java)
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class ChartConfig {
  public String type;
  public ChartData data;
  public ChartOptions options;

  @JsOverlay
  public static ChartConfig bar(String[] labels, String seriesName, double[] values) {
    ChartConfig c = new ChartConfig();
    c.type = "bar";
    c.data = new ChartData();
    c.data.labels = labels;
    Dataset ds = new Dataset();
    ds.label = seriesName;
    ds.data = values;
    c.data.datasets = new Dataset[] {ds};
    return c;
  }
}

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class ChartData {
  public String[] labels;
  public Dataset[] datasets;
}

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class Dataset {
  public String label;
  public double[] data;
}

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class ChartElement {
  public int index;
  public int datasetIndex;
}

Notes on the design:

  • double[] and String[] are Java arrays, which GWT represents as JavaScript arrays, so they are valid as the data and labels properties. The compiler printed no warning for them.

  • CanvasElement is a GWT DOM class (a JavaScriptObject overlay), and passes through the boundary unchanged: a JSO is a plain JavaScript object, so it can be passed as an argument to a native constructor.

  • ChartConfig.bar(...) is a @JsOverlay factory, so the call site reads like a normal Java API while the output is an object literal built from $wnd.Object.

  • ChartOptions.onClick uses the @JsFunction callback shown earlier, with the first parameter typed Object: Chart.js passes an event object that Java never inspects.

The widget wraps the lifetime: the chart exists only while the canvas is attached.

A GWT widget around the native chart (LoanChart.java)
package com.example.lab.client.chart;

import com.google.gwt.dom.client.CanvasElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.user.client.ui.Widget;
import java.util.function.IntConsumer;

/** A GWT widget around the native chart: create on attach, destroy on detach. */
public class LoanChart extends Widget {
  private final String[] months;
  private double[] loans;
  private final IntConsumer onMonth;
  private ChartJs chart;

  public LoanChart(String[] months, double[] loans, IntConsumer onMonth) {
    this.months = months;
    this.loans = loans;
    this.onMonth = onMonth;
    setElement(Document.get().createCanvasElement());
  }

  public void setLoans(double[] values) {
    this.loans = values;
    if (chart != null) {
      chart.data.datasets[0].data = values;
      chart.update();
    }
  }

  @Override
  protected void onLoad() {
    ChartConfig config = ChartConfig.bar(months, "Loans", loans);
    config.options = new ChartOptions();
    config.options.onClick = (event, elements) -> {
      if (elements.length > 0) {
        onMonth.accept(elements[0].index);
      }
    };
    chart = new ChartJs((CanvasElement) getElement().cast(), config);
  }

  @Override
  protected void onUnload() {
    if (chart != null) {
      chart.destroy();
      chart = null;
    }
  }
}

Run in headless Chrome, a page that attached the widget, called setLoans and detached it left this call log in the stand-in library: new bar,update,new bar,update,destroy (the first pair is a chart created directly by the entry point). The destroy at the end is the important line: a native chart library keeps references to the canvas and its event listeners, and GWT’s widget life cycle (onLoad, onUnload) is the right place to release them.

sequenceDiagram participant U as Reader participant C as Chart library (JavaScript) participant F as onClick lambda (Java) participant P as Presenter (Java) U->>C: clicks the bar for March C->>F: onClick(event, elements) F->>P: onMonth.accept(elements[0].index) P-->>U: shows the loans of March Note over C,F: The lambda is a JsFunction, so the library calls it like any JavaScript function

Loading the library is a host-page decision. There are three ways, and the first is the simplest.

Table 3. Three ways to load a JavaScript library
Way How Trade-off

Host page <script> before the nocache script

<script src="chart.min.js"></script> above <script src="bookshelf/bookshelf.nocache.js"></script>. This is what the lab does.

The library loads for every visitor, even those who never see the chart, but it is guaranteed to be there when onModuleLoad starts.

<script> element in the module XML

<script src="..."/> in the .gwt.xml file.

With the default xsiframe linker (see Linkers & Bootstrap) the compile failed when the section’s scratch application was built, unless the configuration property xsiframe.failIfScriptTag was FALSE, in which case the tag was ignored. Use the host page, or the injector below.

ScriptInjector at run time

ScriptInjector.fromUrl(url).setWindow(ScriptInjector.TOP_WINDOW).setCallback(...).inject()

Loads on demand (for example when the admin area opens). The callback tells you when the global is ready; setWindow(TOP_WINDOW) puts it in the same window that $wnd points to.

The lab loaded late.js (a one-line script assigning window.lateLib = 'v1') with ScriptInjector.fromUrl("late.js").setWindow(ScriptInjector.TOP_WINDOW), and the callback read the global back through Js.global().get("lateLib") and printed loaded:v1.

The Developer Guide’s own example does the same for the Leaflet map library: two native classes, L (global) and Map (namespace L), and a one-line call L.map("map").setView(positions, 13) in an EntryPoint. That example comes from a 2016 community repository (zak905/jsinterop-leaflet, last push 2016-05-31), so it shows the technique but its dependency versions are historical.

Exporting Java to JavaScript

Sometimes the caller is not Java: a legacy script on the Bookshelf host page, a Selenium test, a browser extension or a hand-written widget wants to open a book or format a number with the application’s own code. JsInterop makes a Java type reachable from JavaScript by exporting it under a name that the compiler does not obfuscate.

The Bookshelf’s exported API

This is ExportedApi, as it stands in the scratch application.

The Bookshelf’s exported class (bookshelf-client)
package com.example.bookshelf.client.interop;

import com.example.bookshelf.client.ClientFactory;
import com.example.bookshelf.client.place.BookPlace;
import jsinterop.annotations.JsMethod;
import jsinterop.annotations.JsType;

/** Java API exported to JavaScript as {@code bookshelf.Api} (needs -generateJsInteropExports). */
@JsType(namespace = "bookshelf", name = "Api")
public final class ExportedApi {
  private static ClientFactory factory;

  private ExportedApi() {}

  public static void install(ClientFactory f) { factory = f; }

  public static void open(double id) { factory.getPlaceController().goTo(new BookPlace((long) id)); }

  @JsMethod
  public static String formatCopies(String locale, double n) { return new NumberFormatJs(locale).format(n); }
}

Compiling the module printed a warning for it:

Compiler output for the class above
   Warnings in com/example/bookshelf/client/interop/ExportedApi.java
      [WARN] Line 15: [unusable-by-js] Type of parameter 'f' in 'void ExportedApi.install(ClientFactory)' is not usable by but exposed to JavaScript.
   [WARN] Suppress "[unusable-by-js]" warnings by adding a `@SuppressWarnings("unusable-by-js")` annotation to the corresponding member.

The warning is accurate. install(ClientFactory) is public in a @JsType, so it is exported, but ClientFactory is not a @JsType, so no JavaScript caller can supply a meaningful argument. install is an internal hook that only App.onModuleLoad calls. The fix is @JsIgnore, which removes the method from the exports, and it is what the lab’s Api does for its fire method.

A fuller example: the lab’s Api and BookRef

The lab has an exported static API and an exported class with instances.

An exported static API (excerpt of Api.java)
@JsType(namespace = "bookshelf", name = "Api")
public final class Api {
  /** A JavaScript function that Java can call back. */
  @JsFunction
  public interface Listener {
    void on(String event);
  }

  public static final String VERSION = "1.0";

  private static Listener listener;

  private Api() {}

  public static String formatCopies(String locale, double n) {
    return new NumberFormatJs(locale).format(n);
  }

  public static void subscribe(Listener l) {
    listener = l;
  }

  @JsIgnore
  public static void fire(String event) {
    if (listener != null) {
      listener.on(event);
    }
  }

  @JsMethod(name = "sum")
  public static double add(double a, double b) {
    return a + b;
  }
}
An exported class with instances (BookRef.java)
package com.example.lab.client.exported;

import jsinterop.annotations.JsConstructor;
import jsinterop.annotations.JsIgnore;
import jsinterop.annotations.JsProperty;
import jsinterop.annotations.JsType;

/** A Java class used from JavaScript as {@code new bookshelf.BookRef("Dune", 7)}. */
@JsType(namespace = "bookshelf", name = "BookRef")
public class BookRef {
  public String title;
  private final double id;

  @JsConstructor
  public BookRef(String title, double id) {
    this.title = title;
    this.id = id;
  }

  @JsIgnore
  public BookRef(long id) {
    this("#" + id, id);
  }

  @JsProperty
  public double getId() {
    return id;
  }

  public String describe() {
    return title + " (" + (long) id + ")";
  }

  @JsIgnore
  public boolean isSameBook(BookRef other) {
    return other != null && other.id == id;
  }
}
Using both from the host page, and what Chrome printed
bookshelf.Api.formatCopies('de-DE', 1234.5);   // "1.234,5"
bookshelf.Api.sum(2, 3);                       // 5   (Java name add, JavaScript name sum)
bookshelf.Api.VERSION;                         // "1.0"
var b = new bookshelf.BookRef('Dune', 7);
b.describe();                                  // "Dune (7)"
b.id;                                          // 7   (the @JsProperty getter getId)
b.title;                                       // "Dune"
typeof b.isSameBook;                           // "undefined" (@JsIgnore)
typeof bookshelf.Api.fire;                     // "undefined" (@JsIgnore)
  • namespace = "bookshelf" plus name = "Api" produce the global bookshelf.Api; the nested name is created by the generated code.

  • @JsConstructor is the constructor JavaScript’s new calls. The class has a second constructor taking a long; it carries @JsIgnore, because a @JsType class may have only one exported constructor (the pitfalls table shows the three errors the compiler reports without it).

  • @JsProperty on getId() exposes id as a property, b.id, with no parentheses.

  • @JsMethod(name = "sum") renames the Java method add.

  • static final String VERSION is exported as a property of the namespace object.

  • The @JsFunction interface Listener is the type of the argument of subscribe: JavaScript passes a plain function, and Java calls it with listener.on(event).

The compiler emits the exports as a block of assignments. In -style PRETTY output (trimmed to the relevant lines):

Generated export registrations (PRETTY output, lines trimmed)
_ = provide('bookshelf.Api');
_.VERSION = VERSION;
_.formatCopies = formatCopies;
_.subscribe = subscribe;
_.sum = add_0;
_ = provide('bookshelf.BookRef', BookRef);

The Java method add is exported under the JavaScript name sum, and fire does not appear because of @JsIgnore. The member names are exactly the exported JavaScript names even though everything else in the output (add_0, BookRef) is the compiler’s.

The export flags

Exports are generated only when asked for. The flags are printed by Compiler -help and are the same for the code server.

Table 4. The export flags and what they did in the lab
Flag Meaning Observed on 2.13.1

-generateJsInteropExports (-nogenerateJsInteropExports)

Generate the exports requested by the annotations. Default OFF.

Without it typeof bookshelf was undefined and every access threw ReferenceError: bookshelf is not defined. The module itself still ran: the Java-to-JavaScript direction needs no flag.

-includeJsInteropExports regex

Export only members whose qualified name matches. Repeatable. Only effective together with the flag above.

With 'com\.example\.lab\.client\.exported\.BookRef.*' only bookshelf.BookRef existed; bookshelf.Api.sum(2,3) threw TypeError: Cannot read properties of undefined (reading 'sum').

-excludeJsInteropExports regex

Remove matches from the included set. It must come after an include, or the compiler prints -excludeJsInteropExports must be preceeded by -includeJsInteropExports.

Including 'com\.example\.lab\.client\.exported\..*' and excluding '.*BookRef.*' left bookshelf.Api working and made new bookshelf.BookRef(...) throw TypeError: bookshelf.BookRef is not a constructor.

Details worth knowing:

  • The patterns are Java regular expressions, not the package wildcards that the 2.8.1 release notes describe. The class RegexFilter compiles each argument with java.util.regex.Pattern; the .* suffix in the examples above is a regular expression, and the dots in a package name are best escaped. Quote the argument in the shell. A pattern may not start with + or -.

  • Without a filter, everything is exported. In the PRETTY output of the lab, next to bookshelf.Api, the compiler registered java.lang.String, java.util.List and other JRE emulation types (for example _.copyOf = copyOf; under provide('java.util.List')), because GWT’s own emulated classes carry JsInterop annotations. An include filter that names only your packages keeps the output smaller and keeps the global namespace yours.

  • Exports cost bytes and defeat pruning. The measurement in Compiler & Compilation shows a growth from 244,639 to 263,127 bytes of the main file when the flag was on in the Bookshelf.

  • Code server. In Super Dev Mode the flag must reach gwt:codeserver (codeserverArgs), as described under [setup]; the CodeServer class accepts it and its compiled output then contained the exports.

  • The flag is a build choice, not part of the code. The same annotated classes compiled without it simply have no JavaScript entry point; the Java callers are unaffected.

What crosses the boundary

Java and JavaScript do not share a type system, so the values that cross the line need care. The table lists each case that was tried; "in" means a JavaScript caller passing a value to an exported Java method, "out" means a Java value returned or thrown to JavaScript.

Table 5. Values and exceptions across the boundary (GWT 2.13.1, headless Chrome, jsinterop.checks at its production default)
Case What happened Advice

String in: undefined or null

Both arrived as Java null: describe(undefined) and describe(null) returned null-or-undefined; describe('abc') returned len=3.

Test with == null; there is no way to tell the two apart in Java without Js.isTripleEqual.

double in and out

Passed unchanged (sum(2, 3) returned 5).

The safe numeric type of the boundary.

int in

No conversion or range check: plusOne(3.7) returned 4.7 and plusOne(2147483647) returned 2147483648 (no overflow).

An int parameter trusts the caller. Validate, or take a double.

long

The compiler warns [unusable-by-js] Type of parameter 'id' in 'String Api.echoLong(long)' is not usable by but exposed to JavaScript. (and the same for a long return type). A small value did arrive (echoLong(5) returned long=5), but the type is not usable in the JsInterop contract.

Use double for ids that stay below 2^53, or a String. J2CL represents long as a different object (see [j2cl]).

Java array (String[])

A JavaScript array works: joinArray(['a','b']) returned a+b.

Use arrays, not collections, in exported signatures.

java.util.List in

joinList(['a','b']) threw TypeError: elements.iterator is not a function: the JavaScript array is not a Java list.

Take an array and wrap it in Java (Arrays.asList), or accept a JsArrayLike (see [jsinterop-base]).

Object fields in JSON

JSON.stringify of a plain Java object with title and copies printed {}; of the native DTO BookDto it printed {"title":"Dune","copies":3}.

Export or use native DTOs for anything JavaScript must read.

Private state of an exported class

Object.keys(new bookshelf.BookRef('Dune', 7)) printed title,id_0 in PRETTY output and title,a in OBFUSCATED output: the private field id exists as an own property under a compiler name.

Do not rely on, or serialise, the own properties of an exported object; keep the contract in @JsProperty accessors.

Java exception out

bookshelf.Api.throwJava() threw a JavaScript error whose string form was Error: java.lang.IllegalStateException: from Java.

Catch and convert at the boundary if the script needs a particular shape.

JavaScript exception in

A listener that threw new Error('js failed') was caught in Java with catch (Throwable t); t.getClass().getName() was com.google.gwt.core.client.JavaScriptException.

Catch JavaScriptException where a callback may fail.

Missing global in a native binding

With the Shelf script absent, an uncaught-exception handler received java.lang.NullPointerException: TypeError: $wnd.Shelf is not a constructor: a JavaScript TypeError can surface as a Java NullPointerException.

Read the message, not just the class; load the script before the module.

Any.asInt() on a fractional value

intOf(9.5) returned 9.5 and intOf('x') returned x: the check is off in production. With <set-property name="jsinterop.checks" value="ENABLED"/> in the module, intOf(9.5) threw Error: java.lang.ClassCastException and intOf(7) returned 7.

See [jsinterop-base].

jsinterop-base: Js, JsPropertyMap and Any

Annotations describe static shapes. Some JavaScript code is not static: a JSON document whose properties are only known at run time, a value of unknown type, the global object, an array-like. jsinterop-base (com.google.jsinterop:base) is a small library of native types and helpers for these cases. It is written once and compiled by GWT and by J2CL.

Table 6. The classes of jsinterop-base 1.2.0 (from its sources)
Type What it offers

Js

Static helpers: undefined(), global() (the global object as a JsPropertyMap), typeof(Object), asAny, asPropertyMap, asArrayLike, asArray, the conversions asString, asBoolean, asDouble, asFloat, asLong, asInt, asShort, asChar, asByte, cast (a checked cast), uncheckedCast, andAlso, orElse, isTruthy, isFalsy, isTripleEqual, coerceToDouble, coerceToInt, coerceToBoolean, asConstructorFn(Class), arguments() and debugger().

JsPropertyMap<T>

A native type named Object: of(...) factories (zero to three pairs), get, getAsAny, nestedGet and nestedGetAsAny (dotted names), has, set, delete and forEach.

Any

A native type named *: any JavaScript value, with asString, asBoolean, asDouble, asFloat, asLong, asInt, asShort, asChar, asByte, asPropertyMap, asArrayLike, asArray, cast and uncheckedCast.

JsArrayLike<T>

An array-like object: getLength, setLength, getAt, getAtAsAny, setAt, delete and asList.

JsConstructorFn<T>

A JavaScript constructor as a function: construct(args) applies new.

JsBigint, JsForEachCallbackFn

A marker for the bigint primitive, and the callback of JsPropertyMap.forEach.

The lab parses a JSON string and reads it without declaring a DTO:

Dynamic access with JsPropertyMap and Any (excerpt of Lab.java)
JsPropertyMap<Object> book = Js.asPropertyMap(
    JsonNative.parse("{\"title\":\"Dune\",\"copies\":3,\"tags\":[\"sf\"]}"));
Any title = book.getAsAny("title");
String line = "title=" + title.asString()
    + " copies=" + book.getAsAny("copies").asInt()
    + " has(isbn)=" + book.has("isbn")
    + " typeof=" + Js.typeof(book.get("tags"));

In Chrome line was title=Dune copies=3 has(isbn)=false typeof=object. A few notes from the sources and the runs:

  • Js.asPropertyMap(x) and Js.asAny(x) do not copy or wrap: they cast. The result is the same JavaScript value.

  • Js.global() is the global object; Js.global().get("lateLib") returned the value v1 that a script set on window.

  • Js.cast returns its argument with the target type inferred from the assignment (the Java compiler inserts the cast at the call site); Js.uncheckedCast is the unchecked variant, and its Javadoc warns that it lets wrong types spread through the program, so prefer a regular cast.

  • The conversions check, but only when jsinterop.checks is ENABLED. Base.gwt.xml defines jsinterop.checks with the values ENABLED and DISABLED and sets it to DISABLED; its comment says that disabling the checks only affects production. With the default, Js.asAny(9.5).asInt() returned 9.5; with <set-property name="jsinterop.checks" value="ENABLED"/> in the application module it threw ClassCastException.

  • A JsPropertyMap is the right tool for reading JSON of unknown shape; for a stable shape, a native DTO is shorter and typed.

Promises and callbacks

GWT has two callback styles, and the browser has a third.

  • AsyncCallback<T> is the GWT-RPC interface with onFailure(Throwable) and onSuccess(T). It is a normal Java interface, not a JavaScript function.

  • @JsFunction interfaces are JavaScript functions seen from Java, as shown above.

  • Promises are the JavaScript Promise object, which the browser returns from fetch, from Response.json() and from many web APIs. Elemental2 provides elemental2.promise.Promise<T> and IThenable<T> as native types; their methods are then, catch_ (bound to JavaScript’s catch with @JsMethod(name = "catch")), finally_, and the static all, race, resolve and reject.

@JsAsync, the annotation for async functions, has no effect in GWT 2.13.1 (see the annotation table), so a GWT method returns a Promise explicitly instead of declaring async.

Consuming a Promise: fetch

fetch and the JSON it returns (Net.java)
package com.example.lab.client;

import com.google.gwt.user.client.rpc.AsyncCallback;
import elemental2.dom.DomGlobal;
import elemental2.promise.Promise;
import java.util.function.Consumer;
import jsinterop.base.Any;
import jsinterop.base.Js;
import jsinterop.base.JsPropertyMap;

/** Promise-based code next to GWT's callback style. */
public final class Net {
  private Net() {}

  /** fetch() returns a Promise; the chain below never blocks. */
  public static Promise<String> firstTitle(String url) {
    return DomGlobal.fetch(url)
        .then(response -> {
          if (!response.ok) {
            return Promise.reject("HTTP " + response.status);
          }
          return response.json();
        })
        .then(json -> {
          JsPropertyMap<Object> page = Js.asPropertyMap(json);
          Any[] books = page.getAsAny("books").asArray();
          return Promise.resolve(books[0].asPropertyMap().getAsAny("title").asString());
        });
  }

  /** Adapter: expose a callback-style call as a Promise. */
  public static <T> Promise<T> toPromise(Consumer<AsyncCallback<T>> call) {
    return new Promise<>((resolve, reject) ->
        call.accept(new AsyncCallback<T>() {
          @Override
          public void onFailure(Throwable caught) {
            reject.onInvoke(caught.getMessage());
          }

          @Override
          public void onSuccess(T result) {
            resolve.onInvoke(result);
          }
        }));
  }

  /** Adapter the other way: consume a Promise through an AsyncCallback. */
  public static <T> void fromPromise(Promise<T> promise, AsyncCallback<T> callback) {
    promise.then(value -> {
      callback.onSuccess(value);
      return null;
    }, error -> {
      callback.onFailure(new RuntimeException(String.valueOf(error)));
      return null;
    });
  }
}

The lab served a one-book JSON file and called these methods:

  • Net.firstTitle("books.json") resolved to Dune, reading books[0].title through JsPropertyMap and Any.

  • Net.firstTitle("missing.json") rejected with HTTP 404, the string passed to Promise.reject.

  • Net.<String>toPromise(cb -> cb.onSuccess("rpc-ok")) turned a callback-style call into a Promise that resolved to rpc-ok.

  • Net.fromPromise(promise, callback) did the reverse: for the failed fetch it called onFailure with a RuntimeException whose message was HTTP 404.

Three observations about the code:

  • The lambdas passed to then are @JsFunction lambdas, and the generic types (Promise<String>, IThenable<...>) are erased as usual; the Java compiler infers the chain.

  • A promise rejection value can be anything. The adapter converts it with String.valueOf(error); a production adapter should inspect it (for a fetch failure it is a TypeError, for a rejected Response the code above builds the message itself).

  • The callbacks run later, on the browser’s event loop; the GWT code is single-threaded and needs no synchronisation, but a user can navigate away before the Promise settles, so check that the presenter is still attached.

Producing a Promise for JavaScript

An exported method can return a Promise and a JavaScript caller then uses .then and await on it directly.

An exported method that returns a Promise (excerpt of Api.java)
public static Promise<String> titleOf(double id) {
  return new Promise<>((resolve, reject) ->
      Scheduler.get().scheduleDeferred(() -> {
        if (id < 0) {
          reject.onInvoke("no such book: " + id);
        } else {
          resolve.onInvoke("Title #" + (long) id);
        }
      }));
}
Used from JavaScript, and what it printed
bookshelf.Api.titleOf(5)
  .then(function (t) { /* "Title #5" */ return bookshelf.Api.titleOf(-1); })
  .catch(function (e) { /* "no such book: -1" */ });

The Promise constructor takes a @JsFunction executor (resolve, reject), and calling resolve.onInvoke(value) or reject.onInvoke(reason) is how Java settles it. Scheduler.scheduleDeferred is the GWT way to run something after the current event; Delayed Logic & Scheduler covers it.

Web components

A custom element is a JavaScript class registered with customElements.define; from Java it is a native class that extends the DOM element type. The lab registers book-badge in the host page and binds it:

The custom element (host page) and its binding (BookBadge.java)
class BookBadge extends HTMLElement {
  get count() { return Number(this.getAttribute("count")); }
  set count(v) { this.setAttribute("count", v); this.textContent = v + " books"; }
}
customElements.define("book-badge", BookBadge);
window.BookBadge = BookBadge;
package com.example.lab.client.shelf;

import elemental2.dom.HTMLElement;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsProperty;
import jsinterop.annotations.JsType;

/** Binding for a custom element registered by the page: customElements.define("book-badge", BookBadge). */
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "BookBadge")
public class BookBadge extends HTMLElement {
  @JsProperty
  public native int getCount();

  @JsProperty
  public native void setCount(int count);
}
Creating the element from Java
BookBadge badge = (BookBadge) DomGlobal.document.createElement("book-badge");
badge.setCount(3);
DomGlobal.document.body.appendChild(badge);
// badge.getCount() is 3, badge.textContent is "3 books", badge.tagName is "BOOK-BADGE"

window.BookBadge = BookBadge is there because a native class is resolved by its global name: the cast (BookBadge) element compiles to an instanceof $wnd.BookBadge test. The element’s rendering, styling and lifecycle belong to JavaScript; Java sees properties and methods.

The Polymer tutorial, as history

The Developer Guide’s tutorial menu still lists "GWT & Polymer", a six-page tutorial that builds a to-do application with Polymer Web Components through the vaadin-gwt-polymer-elements library. It belongs to the early JsInterop period and should be read as history:

  • It targets the experimental JsInterop of GWT 2.7: its project page tells you to set <jsInteropMode>JS</jsInteropMode> in the old org.codehaus.mojo:gwt-maven-plugin and explains that JsInterop had to be enabled explicitly in 2.7.0. The 2.8.0 (RC1) release notes list the removal of that legacy JsInterop, and the 2.8.0 (Beta1) notes say JsInterop had graduated from experimental, so the flag and the setting no longer exist.

  • It creates the project with the SDK’s webAppCreator command and an old pom.xml fix-up, where this section uses the modular-webapp Maven archetype.

  • The library it uses, com.vaadin.polymer:vaadin-gwt-polymer-elements, has its last Maven Central release (1.9.3.1) dated 2017-09-17; the GitHub repository has moved from vaadin to manolo/gwt-polymer-elements and its last push was 2018-10-23. It wraps Polymer 1.x elements.

  • The Polymer project itself states in its README that LitElement is the successor of PolymerElement and recommends considering it for new projects.

The tutorial still shows the technique that did not age: web components are DOM elements with properties, so they are native Java types, and a code generator (gwt-api-generator) can produce those bindings from the elements' metadata. For a new application use the pattern shown in the section above, or Elemental2’s DOM types.

J2CL compatibility

J2CL is Google’s Java-to-JavaScript transpiler. It is a separate product from GWT, and the J2CL README says the GWT 3 effort evolved into an SDK for libraries and enterprise tooling on top of it. The point for a GWT 2.13 application is not to switch, but to write the boundary code (the annotations and the helper libraries on this page) so that it stays portable: GWT 2.9.0 made the compiler accept jsinterop-base, Elemental2 and jsinterop-annotations 2.0.0, which the release notes call compatibility across these tools with J2CL, except for @JsAsync and @JsEnum.

Table 7. JsInterop in GWT 2.13.1 and in J2CL
Topic GWT 2.13.1 (run in the lab) J2CL (from its documentation) What to do in shared code

The annotations

@JsType, @JsMethod, @JsProperty, @JsConstructor, @JsFunction, @JsOverlay, @JsIgnore, @JsOptional honoured.

The same annotations from the same jsinterop-annotations library.

Use only these; they mean the same in both.

Exports

Nothing is exported unless -generateJsInteropExports; objects are reachable at global names such as bookshelf.Api.

J2CL’s cookbook says the generated Java classes are not directly callable by default because names are mangled, and that the annotations are the way to expose an API; its examples use goog.require("com.google.example.MyClass") (Closure modules), not a global.

Do not assume a global namespace in code that must also build with J2CL.

@JsEnum, @JsAsync

Accepted, no effect.

@JsEnum is documented (a Java enum becomes a JavaScript enum object).

Avoid them in code that GWT 2.13 compiles.

@JsOptional

Honoured; primitive parameters rejected.

Documented; the cookbook recommends also marking the parameter @Nullable.

Use boxed types for optional parameters.

long

[unusable-by-js] warning.

long is represented by goog.math.Long (the semantics page).

Use double or String at the boundary.

Records

Java 17 records compile (see JRE Emulation & Shared Code); their use with JsInterop was not tested here.

The cookbook shows @JsType on a record and @JsProperty on record components.

Test before you rely on records in exported types.

Native JavaScript code

JSNI (/*-{ ... }-*/) is allowed in non-native types.

No JSNI. .native.js files are described as "the equivalent of JSNI" and are discouraged in favour of JsInterop and Elemental2.

Avoid JSNI and generators in shared code; wrap JavaScript with native types.

Platform-specific code

super-source and @GwtIncompatible.

super-j2cl/ folders and @GwtIncompatible.

Keep browser-specific classes small and behind an interface.

The sources of jsinterop-base show the dual-target discipline: Js.java contains // J2CL_ONLY comments before annotations that only J2CL needs, and the JSNI bodies of its helpers exist only for GWT. The GWT roadmap says the project explores supporting some J2CL-exclusive JsInterop features in the GWT compiler, and the org.gwtproject.* J2CL-ready modules are still release candidates (1.0.0-RC1 and 1.0.0-RC2) on Maven Central. J2CL & the Future treats those projects and the migration path.

Moving from JSNI to JsInterop

Code written with JSNI keeps compiling in GWT 2.13.1, but every pattern has a JsInterop counterpart, and the counterpart is usually shorter, typed and checked at compile time. JSNI & Overlay Types keeps the full side-by-side table; the essentials are:

Table 8. The mapping in one table
JSNI or overlay pattern JsInterop replacement

native String format(double n) /*-{ return new $wnd.Intl.NumberFormat().format(n); }-*/;

A native type Intl.NumberFormat with native String format(double) (above).

A JavaScriptObject subclass with protected constructor and final native getters for a JSON object

A native DTO named Object with public fields, or JsPropertyMap for unknown shapes.

$wnd.someGlobal reached through a JSNI method

A native type with namespace = JsPackage.GLOBAL.

$wnd.myApp = { open: $entry(@pkg.Api::open(D)) } to expose Java

An exported @JsType plus -generateJsInteropExports.

A JavaScript callback stored as a JavaScriptObject and invoked with a JSNI helper

A @JsFunction interface.

JSONParser.parse (deprecated; it calls eval) or eval on text

JSON.parse through a native type, then JsPropertyMap or a native DTO; RequestBuilder, JSON & XML covers the security trade-off.

Pitfalls

The compiler messages below were printed by the 2.13.1 compiler for a probe module with one deliberately broken class per row. They appear one per line under the heading Errors in com/.../File.java; when the compiler also prints Ignored 1 unit with compilation errors in first pass. followed by Compile with -strict or with -logLevel set to TRACE or DEBUG to see all errors., the other errors of that run were hidden until the first one was fixed (see Compiler & Compilation for -failOnError).

Symptom (messages quoted as printed) Cause Fix

ReferenceError: bookshelf is not defined in the console when the host page calls the API

The module was compiled without -generateJsInteropExports, or Super Dev Mode runs without it.

Add the flag to compilerArgs and codeserverArgs.

TypeError: Cannot read properties of undefined (reading 'sum')

An -includeJsInteropExports pattern does not match that class (and the global namespace object exists because another class matched).

Fix the regular expression; remember it is matched against qualified member names.

[unusable-by-js] Type of parameter 'f' in 'void ExportedApi.install(ClientFactory)' is not usable by but exposed to JavaScript.

A public member of a @JsType uses a type that JavaScript cannot construct or use (not a @JsType, a long, a Java enum).

@JsIgnore the member, make the type a @JsType, change the type, or @SuppressWarnings("unusable-by-js") when it is intended.

'void Overloaded.find(int)' and 'void Overloaded.find(String)' cannot both use the same JavaScript name 'find'.

JavaScript has no overloading; two public methods of an exported type have the same name.

@JsIgnore one, or give each a name with @JsMethod(name = ...). The same message appears when two @JsMethod(name = "go") collide.

More than one JsConstructor exists for 'TwoCtors'., then Constructor 'TwoCtors.TwoCtors()' can be a JsConstructor only if all constructors in the class are delegating to it.

A @JsType with two public constructors: each is implicitly a @JsConstructor.

Mark one @JsConstructor, delegate from the others (this(...)), and @JsIgnore the ones that must stay Java-only.

'ExportedB.ExportedB()' cannot be exported because the global name 'bookshelf.Same' is already taken by 'ExportedA.ExportedA()'.

Two exported types have the same namespace and name.

Rename one.

Native JsType method 'String NativeWithBody.name()' should be native or abstract.

A native type has a Java method body.

Make the method native, or @JsOverlay and final.

Native JsType constructor 'NativeCtorBody.NativeCtorBody()' cannot have non-empty method body.

A native constructor assigns fields.

Keep it empty; set properties after new, or use a @JsOverlay factory.

Native JsType 'NativeFinal' cannot have initializer. and Native JsType field 'int NativeFinal.fixed' cannot be final.

A field of a native type has an initial value or is final.

Declare plain, uninitialised fields.

JsOverlay method 'void OverlayNotFinal.twice()' cannot be non-final nor native.

@JsOverlay was put on an overridable or native method.

Make it final (or static) with a body.

Native JsType 'NativeExtendsJava' can only extend native JsType classes.

A native class extends a Java class.

Extend another native type, or none.

JSNI method 'String JsniInNative.f()' is not allowed in a native JsType.

JSNI inside a native type.

Use native without a body, so the call goes to the JavaScript member.

Cannot do instanceof against native JsType interface 'NativeIface'.

A native interface has no runtime marker.

Test with a native class, or check a property with Js.isTruthy(map.get("x")).

JsProperty 'int BadProperty.foo()' should either follow Java Bean naming conventions or provide a name.

@JsProperty on a method that is not getX, isX or setX.

Rename it, or give @JsProperty(name = "foo").

JsProperty setter 'void Props.setX(String)' and getter 'int Props.getX()' cannot have inconsistent types.

The getter and the setter of one property have different types.

Use the same type for both.

JsOptional parameter 'n' in method 'void OptionalProblems.b(int)' cannot be of primitive type. and JsOptional parameter 'second' in method 'void OptionalProblems.a(String, String)' cannot precede parameters that are not optional.

@JsOptional on a primitive, or before a required parameter.

Box the type; keep optional parameters last.

Instance member 'void InstanceNs.inst()' cannot declare a namespace.

@JsMethod(namespace = ...) on an instance method.

Namespaces are for static members; drop it.

Local class 'Local.1Inner' cannot be a JsType.

@JsType on a class declared inside a method.

Move it to a top-level or static nested class.

Native JsType member 'void Over.ig()' cannot have @JsIgnore.

@JsIgnore on a member of a native type.

Delete the member instead.

'Fn.Impl' cannot be both a JsFunction implementation and a JsType at the same time. and JsFunction implementation 'Fn.Impl' must be final.

A class that implements a @JsFunction interface is also a @JsType, or is not final.

Implement @JsFunction interfaces with lambdas or final classes without @JsType.

@JsFunction is only allowed on functional interface

The interface has two abstract methods. The message comes from the Java front end and ends with a help URL that is a broken link.

Keep exactly one abstract method.

java.lang.NullPointerException: TypeError: $wnd.Shelf is not a constructor in an uncaught-exception handler

The script that defines the global was not loaded before the module ran new Shelf(...).

Load the script in the host page before the nocache script, or inject it and wait for the callback.

TypeError: elements.iterator is not a function

A JavaScript array passed where Java expects a List.

Use an array in the exported signature.

intOf(9.5) returns 9.5

The jsinterop-base conversions check only when jsinterop.checks is ENABLED.

Validate in Java, or enable the property in development.

JSON.stringify(javaObject) printed {}; keys such as id_0 show up on exported objects

Java fields are not stable JavaScript properties.

Use native DTOs for data, and @JsProperty accessors on exported types.

The chart keeps listeners after the widget is removed

A native library object was created in onLoad and never released.

Call destroy() in onUnload.

The Bookshelf, end to end

In the section’s running scenario, JsInterop appears in three places, each one a pattern from this page.

  • Number formatting (native type). NumberFormatJs formats the number of copies in the reader’s locale (see Internationalization); the exported bookshelf.Api.formatCopies makes the same function reachable from the host page.

  • Opening a book from outside GWT (exported type). bookshelf.Api.open(id) calls the PlaceController (see MVP, Activities & Places), so a legacy script, a link on another page of the site or a test can navigate the application without reaching into its internals. The install(ClientFactory) hook is Java-only (@JsIgnore).

  • The admin chart (native type, callback, widget). LoanChart wraps the chart library, calls destroy() when detached, and routes onClick back into the presenter.

The pieces fit the earlier pages: the presenters stay in plain Java and know nothing about JavaScript, the native types are in one interop package, and the host page decides which scripts load. Security rules from the other pages still apply across the boundary. A value that came from JavaScript is untrusted input like any other: bookshelf.Api.open(id) must check that the id is a valid number (an int parameter would not, as the table above shows), the server must authorise every operation behind it, and text from a script that is shown on the page goes through setText or SafeHtml (see SafeHtml & XSS), never setHTML with concatenated input.

What changed since the older material

  • JSNI chapter. The 2007 book (GWT 1.3 and 1.4) treats JavaScript interop as JSNI: handwritten JavaScript in /*-{ ... }-*/ comments, $wnd and $doc, JavaScriptObject wrappers and hand-built exports, including loading and wrapping third-party libraries. That material still compiles, but JsInterop (stable since 2.8.0, October 2016) replaced it, and the whole JsInterop vocabulary on this page postdates the book by years.

  • Experimental flag. The -XjsInteropMode flag of the 2.7 line and the <jsInteropMode> plugin setting no longer exist.

  • Annotations jar. The annotations moved to their own artifact com.google.jsinterop:jsinterop-annotations (2.8.0 notes); GWT 2.9.0 aligned with jsinterop-base 1.0.0, Elemental2 1.0.0 and jsinterop-annotations 2.0.0.

  • Export filters. -includeJsInteropExports and -excludeJsInteropExports came in 2.8.1; their notes describe wildcards, and the 2.13.1 implementation uses regular expressions.

  • Overlay on default methods. Since 2.8.0 @JsOverlay may be used on Java 8 default methods (as jsinterop-base does), and @JsOptional was added in the same line.

  • Stale links. The Developer Guide’s annotation Javadoc link and the DevGuideJsInterop.html name used elsewhere both return 404; the working page is DevGuideCodingBasicsJsInterop.html.