GWTReact/gwt-interop-utils

What you call “Object literals” aren't, this is misleading; find another name.

tbroyer opened this issue · 3 comments

“Literals” are a syntactic construct, with few if any impact at runtime. Actually, even the ECMAScript spec doesn't call them “literals”, but “initializers”:

An object initializer is an expression describing the initialization of an Object, written in a form resembling a literal.

— Source: http://www.ecma-international.org/ecma-262/5.1/index.html#sec-11.1.5 & http://www.ecma-international.org/ecma-262/6.0/index.html#sec-object-initializer

From the gwt-interop-utils documentation:

Many javascript libraries make extensive use of object literals ({a : 10, b : "Some value"}) to pass parameters and return results.

Libraries themselves don't make use of object literals (object initializers); developers using them make use of object initializers. Libraries take objects as method parameters, and developers choose to use object initializers to fulfill them.
(I'm obviously not talking about the usage inside the library, which is irrelevant to this discussion)

From the library's side, the method receives an object; and the library cannot really tell (possibly even can't actually tell at all, I haven't studied the ECMAScript spec enough, but anyway it'd be through “hacks”) whether the received object has been constructed using an “object initializer” construct or not.
I.e., inside the library, the following two snippets cannot be distinguished:

library.fn({a: 10, b: "Some value"});
// vs.
library.fn((function() { var o = new Object(); o.a = 10; o.b = "Some value"; return o; })());

Actually, you specifically rely this to construct objects the latter way rather than the former.

So please use another name than “object literal”. The only thing ObjLiteral (or the recommended @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object") on custom types) does is to make sure the object is of the "raw Object type", or in other words, that its constructor is $wnd.Object (and that in turns it has an undefined prototype).
And even this should be the exception rather than the norm: libraries should in most cases accept any object, whichever its constructor, as long as it's not an Array.isArray(). Your use case is driven by some obscure React/Redux limitation (could you point me to the actual methods failing when passing objects whose constructor is not Object?) that aren't the “norm” (to my knowledge) in the JS ecosystem.

May I suggest using raw object or simple object instead?

Also, BTW, ObjLiteral is not much different from StringMap, the main exception being that it's not typed; similar to the com.google.gwt.core.client.JsArray flavors vs. com.google.gwt.core.client.JsArrayMixed.

There is a way to make real object initializers with GWT 2.8 (pending support in GWT proper) using JSNI and without the need for any helper library:

public native WhateverTheFunctionExpects create(int a, String b) /*-{
  return { a: a, b: b };
}-*/;

and you could possibly make it easier to write through an annotation processor to generate the JSNI code (e.g., for each @JsType(isNative = true), generate a <TypeName>_Factory class with static factory methods for each “property-presence” permutation (so I could have a create(int a) and create(String b) in addition to create(int a, String b); and when there are multiple parameters of the same type, use specific method names to disambiguate them, e.g. createWithA(int a) and createWithA(int a, String b) vs. createWithC(int c) and createWithC(String b, int c) vs. create(int a, String b, int c)).

You make a good point about the name being confusing. In the Javascript community they call them Plain Javascript objects i.e. they have no prototype of their own. For example, lodash has functions for detecting them (https://lodash.com/docs#isPlainObject). So perhaps a better name would be PlainJavascriptObj or PlainJsObj.

There are more libraries than just React (and it's ecosystem) that place limitations on using these plain objects. I have come across cases where API's check for them, specifically related to the ability to serialize and deserialize them to/from JSON.

I agree you can use JSNI but I am trying to localize it's usage in new code as much as possible. There is also some added flexibility to being able to add/remove properties at run-time, especially with react. I don't think it hurts to have this capability.

I will go ahead and rename the class and update the documentation.

@tbroyer

Good points!

May I suggest to avoid the telescoping arguments anti-pattern, so widely used and encouraged in the JS community for constructing objects, and use Builders instead. Here is an example from wrapping Google Analytics (I will publish it soon):
Google Analytics JS API:

ga('send', 'social', [socialNetwork], [socialAction], [socialTarget], [fieldsObject]);

JsInterop API user would be able to do this (very readable, less error prone, well typed):

// Rgeister social network event in google analytics and construct complex object with builders
new SocialInteraction.Builder()
           .withSocialNetwork("facebook")
           .withSocialAction("liked")
           .withSocialTarget("whatever")
.register();

JsInterop code wrapper:

@JsType(isNative = true, namespace = JsPackage.GLOBAL)
public class SocialInteraction extends GoogleAnalyticsCommand{

    @JsProperty
    public String socialNetwork;

    @JsProperty
    public String socialAction;

    @JsProperty
    public String socialTarget;

    public static class Builder extends BaseBuilder<Builder>{

       final  private SocialInteraction socialInteraction = new SocialInteraction();

        public Builder withSocialNetwork(String socialNetwork) {
            socialInteraction.socialNetwork = socialNetwork;
            return this;
        }

        public Builder withSocialAction(String socialAction) {
            socialInteraction.socialAction = socialAction;
            return this;
        }

        public Builder withSocialTarget(String socialTarget) {
            socialInteraction.socialTarget = socialTarget;
            return this;
        }

        @Override
        public void register() {
            Objects.requireNonNull(socialInteraction.socialNetwork, "socialNetwork cannot be empty");
            Objects.requireNonNull(socialInteraction.socialAction,  "socialAction cannot be empty");
            Objects.requireNonNull(socialInteraction.socialAction,  "socialTarget cannot be empty");
            if (Variables.isFunctionDefined("ga")) {
                socialInteraction.hitType = "social";
                GoogleAnalytics.ga(buildSendCommand(), socialInteraction);
            }
        }
    }

}

Renamed ObjLiteral to JsPlainObj and updated the documentation