Common pitfalls when using typedefs in externs

Thursday, January 5, 2017 | 9:02 AM

When writing typedefs, it is easy to make mistakes and write definitions that are not supported by Closure Compiler. The source of the issue is that type checking for typedefs is unfortunately quite loose, especially for typedefs defined in externs files. As a result, the compiler does not warn about some malformed definitions. Then, the user thinks their code is getting typechecked when it is not. (Checking is stricter with the new type checker. You may want to consider it for your project.)

The goal of this post is to show the common pitfalls, and suggest correct ways to define typedefs. This will alleviate the problem until the typedef-related checks are enforced by the compiler.

What is a namespace?


In addition to the built-in types such as number, Object, etc, a programmer can define and use their own types: classes, interfaces, records, and typedefs. If a user-defined type is defined as a property of an object, this object must be a namespace. For example:
/**
 * An object-literal namespace
 * @const
 */
var ns = {};

/**
 * A new class Foo as a namespace property
 * @constructor
 */
ns.Foo = function() {};

/** 
 * This is also OK. The new class is not defined as a property.
 * @constructor
 */
function Foo() {}

Object literals defined with @const are namespaces. So are classes and functions. Typedefs are not namespaces.

Do not define new types on typedefs


Since a typedef is not a namespace, you should not define new types on a typedef. The compiler may not warn, but it will not check such types correctly.
// Externs

/** @typedef {{prop1: number, prop2: string}} */
var MyType;

/**
 * Wrong!
 * @constructor
 */
MyType.Foo = function() {};

// Source

/** @param {MyType} x */
function f(x) {
  var /** null */ n;
  n = new MyType.Foo; // Typechecked
  n = new x.Foo; // Typed as unknown
}
You can see that MyType.Foo is not properly checked. It is checked when we refer to it directly, but not on a formal parameter x typed MyType. You can try this example using the debugger. Note that if we had defined MyType in the source instead of the externs, the compiler would have warned.

What about other properties on typedefs?


We saw that defining new types on typedefs is not supported. More generally, you should not define any extra properties on typedefs, even if these properties are not creating new types. The compiler may not warn, but it will not check these properties correctly.

A common mistake people make is to define a typedef of a record type, and then define extra properties on it. The compiler will silently not typecheck in this case.
// Externs

/** @typedef {{prop1: number, prop2: string}} */
var MyType;

/**
 * Wrong!
 * @type {number}
 */
MyType.prop3;

/**
 * Wrong!
 * @type {number}
 */
MyType.prototype.prop4;

// Source

/** @param {MyType} x */
function f(x) {
  var /** null */ n;
  n = x.prop3; // Typed as unknown
  n = x.prop4; // Typed as unknown
}
The correct way to do this is with @record.
/** @record */
function MyType() {}

/** @type {number} */
MyType.prototype.prop1;

/** @type {string} */
MyType.prototype.prop2;

/** @type {number} */
MyType.prototype.prop3;

/** @type {number} */
MyType.prototype.prop4;
A related mistake is to define a typedef of a function, and then define extra properties. Again, some typechecking will silently not happen.
// Externs

/** @typedef {function(number):number} */
var MyType;

/**
 * Wrong!
 * @type {number}
 */
MyType.prop3;

/**
 * Wrong!
 * @type {number}
 */
MyType.prototype.prop4;

// Source

/** @param {MyType} x */
function f(x) {
  var /** null */ n;
  n = x.prop3; // Typed as unknown
  n = x.prop4; // Typed as unknown
}
Closure Compiler does not fully support functions with properties. They are representable in the type system, but there is no notation to write such types.

There is a somewhat hacky way to express functions with properties. Even though it is not entirely correct, it provides some type checking. We first define a typedef MyType of the function type. Then, we define a variable or property of type MyType, and then we define the extra properties on the variable.
// Externs

/** @typedef {function(number):number} */
var MyType;

/** @type {MyType} */
var MyType_;

/** @type {number} */
MyType_.prop3;

/** @type {number} */
MyType_.prop4;
Defining functions with properties this way has two caveats. First, even though these properties should only be defined on MyType, they are defined on more functions, so the compiler misses some inexistent-property warnings. Second, the old and the new type checker behave differently on such types. In this example, the new type checker does not warn but the old one does. Despite these caveats, this is an acceptable workaround to declare types of functions with properties.

0 comments: