Introducing the @struct and @dict annotations for constructors

494 views
Skip to first unread message

Dimitris Vardoulakis

unread,
Oct 31, 2012, 1:33:49 PM10/31/12
to closure-comp...@googlegroups.com
We recently added two new annotations in the Closure compiler, @struct and @dict.

When you annotate a constructor C with @struct, the compiler checks that you only access the properties of C objects using dot notation. It also checks that you do not add new properties to C objects after they're created.

/** 
 * @constructor
 * @struct
 */
function Foo(x) {
  this.x = x;
}

var obj = new Foo(123);
var n1 = obj['x']; // warning
var n2 = obj.x; // OK
obj.y = "asdf"; // warning

When you annotate a constructor C with @dict, the compiler checks that you only access the properties of C objects using brackets.

/** 
 * @constructor
 * @dict
 */
function Foo(x) {
  this['x'] = x;
}

var obj = new Foo(123);
var n1 = obj.x; // warning
var n2 = obj['x']; // OK

So, structs behave like Java objects, and dicts are dictionaries. By using @struct, you know that the compiler will rename all properties safely, because you can't use bracket access. By using @dict, you know that the properties will have the same name after compilation.

You can also use @struct and @dict on object literals directly.

var s = /** @struct */ { x: 1 }, d = /** @dict */ { y: 321 };
var n1 = s[‘x’]; // warning
var n2 = d.y; // warning

This is how the new annotations work in a nutshell. Read on to find out all the intricacies of the new annotations.

More details

Inheritance maintains the annotations

Consider the following example.

/** @constructor */
function Foo(x) {
  this.x = x;
}

/** 
 * @constructor
 * @struct
 * @extends {Foo}
 */
function Bar(x, y) {
  this.x = x;
  this.y = y;
}

/** @param{Foo} obj */
function getx(obj) { return obj[‘x’]; }
var z = getx(new Bar(123, 456)); // no warning

In this example, Bar inherits from Foo and is also annotated with @struct. But then, a function that takes Foos can use bracket access for Bar objects, because Bar is a subtype of Foo. For this reason, the compiler does not allow a struct or dict class to extend an unannotated class. Also, when Bar extends Foo and Foo is annotated with @struct or @dict, Bar automatically inherits the annotation.

Annotating the prototype

Since annotated constructors can’t inherit from unannotated ones, when you annotate a constructor with @struct, you must also annotate the prototype.

One way to do this is to create a constructor for the prototype and annotate that constructor.

/** 
 * @constructor
 * @struct
 */
function FooProto() {
  this.identity = function(x) { return x; };
  this.add1 = function(x) { return x+1; };
}

/** 
 * @constructor
 * @struct
 */
function Foo(x) {
  this.x = x;
}

Foo.prototype = new FooProto();
Foo.prototype.sub1 = function(x) { return x-1; }; // warning

This is verbose because you create FooProto and only use it once. Alternatively, you can use an annotated object literal.

/** 
 * @constructor
 * @struct
 */
function Foo(x) {
  this.x = x;
}

Foo.prototype = /** @struct */ {
  id: function(x) { return x; },
  add1: function(x) { return x+1; }
};

Foo.prototype.sub1 = function(x) { return x-1; }; // warning

Subtyping and unsoundness

There are a few cases where the compiler cannot enforce the properties of structs and dicts. Fixing those cases would require breaking changes to the type system, so we decided to live with some missed checks. 
First, structs and dicts are subtypes of Object. The compiler cannot enforce the struct/dict properties for the Object type.

/** 
 * @constructor
 * @dict
 */
function Foo(x) {
  this[‘x’] = x;
}

/** @param{Object} obj */
function fun1(obj) { return obj.toString(); }

fun1(new Foo(123)); // dict accessed with dot but no warning

Second, if a struct or dict implements an interface, it’s a subtype of that interface. The compiler cannot enforce the struct/dict properties for the interface type.

/** @interface */
function Foo() {}

/**
 * @constructor
 * @struct
 * @implements {Foo}
 */
function Bar() { this.x = 123; }

var n = /** @type{Foo} */(new Bar())[‘x’];

By casting to Foo, the compiler doesn't warn about the illegal bracket access.

Last, structs and dicts can be used in places that expect a record type, like any other class. The compiler cannot enforce the struct/dict properties for the record type.

/** 
 * @constructor
 * @struct
 */
function Foo() { this.x = 123; }

/** @param{{x: number}} rec */
function fun1(rec) { return rec['x']; }

fun1(new Foo()); // struct accessed with brackets but no warning

Guido Tapia

unread,
Nov 1, 2012, 5:52:52 AM11/1/12
to closure-comp...@googlegroups.com
Looks great, would love to see struct be the default @constructor paradigm, however the subclass:

Foo.prototype.sub1 = function(x) { return x-1; }; // warning

Is a problem, why shouldn't a subclass be able to define its own 'struct'ness, if it does not violate its parent's?

Can't wait to play with these new features, well done guys.

Dimitris Vardoulakis

unread,
Nov 1, 2012, 12:13:47 PM11/1/12
to closure-comp...@googlegroups.com
Technically if Kid extends Parent and Parent is @struct, Kid doesn't have to be @struct. However, we thought it would be simpler if the whole inheritance chain behaves in the same way. We'll have to wait and see how people use @struct/@dict in practice to find out if that was a good decision.

Alexey Lakutin

unread,
Nov 6, 2012, 6:13:53 AM11/6/12
to closure-comp...@googlegroups.com
What is warning category for them? (http://code.google.com/p/closure-compiler/wiki/Warnings)
I want to set them error level, 'cos all our @dicts are server-side generated JSON objects and props names should not be minimized.

Dimitris Vardoulakis

unread,
Nov 6, 2012, 1:34:58 PM11/6/12
to closure-comp...@googlegroups.com
We don't have a separate warning category for them. They are always enabled when type checking is enabled.

Alexey Lakutin

unread,
Nov 7, 2012, 8:35:18 AM11/7/12
to closure-comp...@googlegroups.com
Thanks, Dimitris!

Please, tell, is it possible to use such annotations anywhere else? E.g. function parameters, types descriptions.
I'll try to describe use-case.
We have a lot of code, that we need to adapt to ADVANCED mode.
There are too much code like this now:
var json = Ajax.getFromServer(...);
if (json.prop1)
    this.prop1 = json.prop1;
// So on...

var json = {prop1: "val1", etc};
Ajax.sendToServer(json, ...);

json here is plain Object.
We use access by dot operator, which does not protect properties names from obfuscation.
I want to catch all such places at compile time to convert them to [""] operator.
It would be great if I could define return type of Ajax.getFromServer method as @dict and let compiler do the job.
Well, I can define new type with @dict annotation.
But, I'm unable to describe sending object with this type without type casting (it's not very safe, I guess).
What is right solution can be at this moment?

Dimitris Vardoulakis

unread,
Nov 7, 2012, 12:45:30 PM11/7/12
to closure-comp...@googlegroups.com
It's only possible to use @struct/@dict on constructors and object literals. For your use case, you can define a class that describes what you expect back from getFromServer. At minimum, sth like

/**
 * @constructor
 * @dict
 */
function Dict() {}

Then, you can use the annotation @return{Dict} for getFromServer.

Daniel Steigerwald

unread,
Nov 8, 2012, 5:55:36 PM11/8/12
to closure-comp...@googlegroups.com
I chose a different solution. I don't like endless properties cloning and type casting too. Your solution with class will not work if we use properties to match element name attributes.. in short, after long thinking a decided to use models with string based properties. Such approach has another pros:

1) plain json has several inherited properties and we are not able to create blank object for IE<whocare

2) models with set and get can dispatch events with changes

Implementation: https://github.com/Steida/este/blob/master/assets/js/este/model/model.coffee

Dne středa, 7. listopadu 2012 14:35:18 UTC+1 Alexey Lakutin napsal(a):

Guido Tapia

unread,
Jan 20, 2013, 9:26:27 PM1/20/13
to closure-comp...@googlegroups.com
I've been playing with this stuff today and I must say that the following:

Since annotated constructors can’t inherit from unannotated ones, when you annotate a constructor with @struct, you must also annotate the prototype.

Adds huge friction when trying to convert existing code to new safer structs.  The fact that I cannot extend a non-struct is also a bit painful, as it means most of my Disposables, EventHandlers, etc (common base classes) cannot be structs.

My 2 cents

Dimitris Vardoulakis

unread,
Jan 21, 2013, 12:05:07 AM1/21/13
to closure-comp...@googlegroups.com
It's unsafe to have structs extend non-structs. If it were allowed, the value of type-checking structs would diminish, because it's easy to bypass the checking accidentally. 

Why can't you annotate the whole inheritance chain with struct in your code?

Guido Tapia

unread,
Jan 21, 2013, 12:19:52 AM1/21/13
to closure-comp...@googlegroups.com

Why can't you annotate the whole inheritance chain with struct in your code?


I can if its my own hierarchy, but most of my object hierarchies start from a closure library base class.  goog.Disposable being the most common.

Dimitris Vardoulakis

unread,
Jan 21, 2013, 12:26:11 AM1/21/13
to closure-comp...@googlegroups.com
Good point. It'd be nice if the Closure classes were annotated with @struct/@dict. Hopefully someone will take the time to do it sometime soon.


On Sun, Jan 20, 2013 at 9:19 PM, Guido Tapia <guido...@gmail.com> wrote:

Why can't you annotate the whole inheritance chain with struct in your code?


I can if its my own hierarchy, but most of my object hierarchies start from a closure library base class.  goog.Disposable being the most common.



--
Dimitris

Guido Tapia

unread,
Jan 21, 2013, 3:44:20 PM1/21/13
to closure-comp...@googlegroups.com
On Monday, January 21, 2013 4:26:11 PM UTC+11, Dimitris Vardoulakis wrote:
Good point. It'd be nice if the Closure classes were annotated with @struct/@dict. Hopefully someone will take the time to do it sometime soon.


 The problem with this is this:

when Bar extends Foo and Foo is annotated with @struct or @dict, Bar automatically inherits the annotation

I would personally do the following:
* If a constructor is marked as @struct then the compiler should assume the prototype is also a @struct.  Having to make a separate prototype object literal just to annotate it as struct is really cumbersome.
* A class should be allowed to be a struct regardless of its inhertance chain.  
* Child classes should not inherit structness.

An the validation rules should be simplified.  If I have a non struct BaseType and a struct ChildType; And I have a method that takes a BaseType then I should not enforce structness even if the instance of the object is a ChildType.  I think this would get rid of all those problematic intricacies hat I assume lead to the original design decisions.  

I know its probably too late for design comments like this but I wanted to give my feedback after wrestling with this for a few hours yesterday.  I think struct is really useful but for me at least a little too finicky to implement.

Maybe this is because I already have a very large code base.  If starting on a new project with new paradigms then perhaps I would not be whinging so much :)

Dimitris Vardoulakis

unread,
Jan 30, 2013, 7:43:18 PM1/30/13
to closure-comp...@googlegroups.com


On Monday, January 21, 2013 12:44:20 PM UTC-8, Guido Tapia wrote:
On Monday, January 21, 2013 4:26:11 PM UTC+11, Dimitris Vardoulakis wrote:
Good point. It'd be nice if the Closure classes were annotated with @struct/@dict. Hopefully someone will take the time to do it sometime soon.


 The problem with this is this:

when Bar extends Foo and Foo is annotated with @struct or @dict, Bar automatically inherits the annotation

I would personally do the following:
* If a constructor is marked as @struct then the compiler should assume the prototype is also a @struct.  Having to make a separate prototype object literal just to annotate it as struct is really cumbersome.
* A class should be allowed to be a struct regardless of its inhertance chain.  

(Sorry for the late reply.)

I don't think it's late for design comments, since struct/dict are relatively recent and not in widespread use yet.

However, it's not desirable to allow structness regardless of the inheritance chain. This is not sound, and limits what the compiler can do with structs. If Foo which is a struct inherits from Bar which isn't a struct, then I can pass a Foo to a function that takes Bars and suddenly start adding new properties to it, and violate the struct properties. With the current design, the compiler protects you from that. Also, the compiler knows that it can safely rename all properties of structs, because they're always accessed with dot. If we allowed structs to inherit from non structs, the compiler can only provide very weak guarantees.

But you're right that Closure should be annotated, otherwise it'll be hard for the new features to catch on. I'll look into adding the annotations (depending on how much time it will take, I have to ask around a bit first).

John Lenz

unread,
Jan 30, 2013, 8:39:32 PM1/30/13
to closure-comp...@googlegroups.com

I dont think this is true. Object and unknown allow property additions and extern definitions prevent renaming. What it does do is prevent accidential property additions and I think requiring subclasses (not superclasses) to be struct is what we want.

--
 
---
You received this message because you are subscribed to the Google Groups "Closure Compiler Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to closure-compiler-d...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Dimitris Vardoulakis

unread,
Jan 30, 2013, 11:04:37 PM1/30/13
to closure-comp...@googlegroups.com
On Wed, Jan 30, 2013 at 5:39 PM, John Lenz <conca...@gmail.com> wrote:

I dont think this is true. Object and unknown allow property additions and extern definitions prevent renaming.

Yes, you can subvert the check if you want, but the point is to allow that in as few cases as possible, so that missteps are harder.

Requiring structs to only extend structs is exactly the same as saying that when you override a method you must respect its type, it's the same idea.



--
Dimitris

John Lenz

unread,
Jan 31, 2013, 2:56:14 PM1/31/13
to closure-compiler
I think we can have our cake and eat it to hear. I simply put the
"super class isn't an @struct" warning into its own diagnostic group.
Then we can suppress/disable it so that we can use it with library
code.

Stanimir Mladenov

unread,
Feb 1, 2013, 3:50:26 AM2/1/13
to closure-comp...@googlegroups.com
What about solving inheriting from Closure Library (or any other
library) class by type casting it via goog.inherits.

goog.inherits(MyComponent, /** @struct */ (goog.ui.Component));

This could propagate the casting to all parent prototypes and even let
us change from @dict to @struct if the concept allows it.

2013/1/31 John Lenz <conca...@gmail.com>:

Dimitris Vardoulakis

unread,
Feb 5, 2013, 7:59:27 PM2/5/13
to closure-comp...@googlegroups.com


On Thursday, January 31, 2013 11:56:14 AM UTC-8, John wrote:
I think we can have our cake and eat it to hear.  I simply put the
"super class isn't an @struct" warning into its own diagnostic group.
Then we can suppress/disable it so that we can use it with library
code.


I'm fine with that as long as the default for the warning is ON, and then people can suppress it as they wish.

Reply all
Reply to author
Forward
0 new messages