One of the requirements for using ADVANCED_OPTIMIZATIONS with Closure-compiler is providing the compiler with type information for all of the functions and variables that are not directly defined by your code. This is typically done by using an extern file. Extern files are analogous to C/Java header files in that they provide forward type declarations. While writing an extern file is easy, there are a few techniques used that are less than obvious.
This article assumes that you are already familiar with Closure-compiler, it’s restrictions and how to use JSDoc tags.
Let’s start with the basics rules. Externs:
- Are still valid javascript – they just don’t do anything
- Should not contain actual values or definitions – just the type information
- Are included in the compilation with the –externs flag which can occur multiple times
In their simplest form, extern files are just a list of symbol names.
var myNamespace; myNamespace.subgroup; var myGlobalVar; |
This alone is enough to prevent the compiler from renaming these symbols when referenced in your code. In fact, you won’t get errors or warnings either. But don’t do this – you haven’t provided any type information to the compiler and stopping here would bypass many of the coding checks of the compiler as it will assume that these symbols can be literally any type.
Instead, we need to provide type information as well. Here’s how to get started:
Namespaces
Since coding best practice dictates that we should not pollute the global namespace, let’s start by declaring our own namespace.
/** * A typical namespace declaration * Here we use an assignment so the compiler will infer the type. * @const */ var myNamespace = {}; /** * Further namespace segmentation * @const */ myNamespace.subgroup = {}; |
Namespaces are (usually) just empty objects. Using @const improves type checking and enables further optimizations within the compiler.
Variables
/** * A global variable - note there is no assignment needed * @type {number} */ var myGlobalVar; /** * My variable as a static namespace property - still no assignment * @type {number} */ myNamespace.myVal; |
Functions
/** * A global function - note that the definition is completely missing * @param {number} param1 */ function myGlobalFunction(param1) {} /** * A global variable with a function expression type * Just a different way to write the type information * @type {function(number)} */ var myGlobalFunctionExpr2; /** * A global variable assigned a function expression * We have an assignment here so that we can have an @param tag reference * @param {number} param1 */ var myGlobalFunctionExpr3 = function (param1) {}; /** * A function expression as a static property on a namespace * @param {number} param1 */ myNamespace.functionExpr = function (param1) {}; |
One gotcha with extern functions: they MUST contain at least one JSDoc tag or the compiler will not type check them (it will assume they take any number of arguments of any type and can return anything).
/** * This function takes no arguments * It also doesn't return anything * This tag is just to force the compiler to type check it * @return {undefined} */ function myParameterlessFunction() {} |
Constructors and prototypes
With the exception of the @constructor tag, these are just variations on the above definitions.
/** * My type constructor - I will be called with the new keyword * @constructor * @param {boolean=} param1 - an optional argument */ myNamespace.MyType = function(param1) {}; /** * An instance method * @return {number} */ myNamespace.MyType.prototype.myInstanceFunction = function() {}; |
But what about constructors that can be optionally called WITHOUT using the new keyword? You add an @return tag to denote this case.
/** * My type constructor - I may be called without the new keyword * I usually contain code such as: * if (!(this instanceOf myNamespace.MyType2)) * return new myNamespace.MyType2(); * The exclamation mark is used because I will never return null * @constructor * @return {!myNamespace.MyType2} */ myNamespace.MyType2 = function() {}; |