ClojureScript

Externs

This guide requires ClojureScript 1.10.238 or later and assumes familiarity with the Quick Start.

This page documents how to write externs for third party JavaScript libraries that do not conform to Google Closure Compiler conventions https://developers.google.com/closure/compiler/docs/limitations.

Motivation

Many useful libraries cannot go through Google Closure Compiler advanced compilation. Thus they cannot be a part of the build and are considered "foreign". Still Closure must know something about these libraries, otherwise properties may be unintentionally renamed. Unfortunately, often this accidental renaming won’t be apparent until the least opportune time - production.

For libraries that already have mature externs this type of mistake is easily avoided. However, this requirement adds an incredible amount of friction to the adoption of newer or less popular but equally useful libraries. With the arrival of externs inference, the ClojureScript compiler can now automatically generate missing externs as well as greatly aid the process of writing comprehensive externs.

Externs Inference

Imagine that we have specified a foreign library some.fooLib. We would like to write interop against this library but have certainty that either the correct externs will be automatically generated or the compiler will notify us of externs we must additionally supply.

To enable externs inference, we specify the :infer-externs true in our compiler configuration.

Create a build.edn file with the following content:

{:main my-project.core
 :output-to "out/main.js"
 :output-dir "out"
 :optimizations :none
 :infer-externs true})

However this alone isn’t enough to have the compiler generate warnings around externs. Because of the large number of libraries written before this feature existed, we cannot enable this capability in a global way. Instead there is a new file local compiler flag *warn-on-infer* which is somewhat analogous to *warn-on-reflection* in Clojure. Once set the compiler will warn for the remainder of the file anytime it cannot determine the types involved in a dot form, whether property access or method invocation.

(ns my-project.core
  (:require [some.fooLib]))

(set! *warn-on-infer* true)

(defn wrap-baz [x]
  (.baz x))

The above code would trigger a warning message:

Cannot infer target type in expression (.baz x) ...

We simply need to type-hint x with the foreign type for this interop call:

(ns my-project.core
  (:require [some.fooLib]))

(set! *warn-on-infer* true)

(defn wrap-baz [^js/Foo.Bar x]
  (.baz x))

The compiler now has enough information to automatically generate the required externs. When you run your build you will see a new file in your output directory inferred_externs.js. If you examine its contents it will probably look similar to the following:

var Foo = {};
Foo.Bar = function() {};
Foo.Bar.prototype.baz = function() {};

Quickly integrating foreign JavaScript libraries without complete externs is now considerably easier and less error prone.

In some cases you may still want to write externs or you may be a consumer of a popular JavaScript library with mature externs and you would like a bit more validation. The following section describes an additional useful feature provided by externs inference.

Return Types

Local type hints go a long way to automating the process of writing externs. However, for interop heavy code this will lead to a lot of type hinting particularly for the return values of commonly used functions. In this case it’s probably better to provide the externs file. Even here the ClojureScript compiler can ease the process:

(ns my-project.core
  (:require [some.fooLib]))

(set! *warn-on-infer* true)

(defn my-fn [^js/Foo.Bar x]
  (let [z (.baz x)]
    (.-wozz z)))

Imagine that our externs file looks something like the following:

var Foo = {};
/**
 * @constructor
 */
Foo.Bar = function() {};
Foo.Bar.prototype.baz = function() {};
/**
 * @constructor
 */
Foo.Boo = function() {};
Foo.Boo.prototype.woz = function() {};

However this isn’t sufficient for knowing the type of z in the ClojureScript program. The ClojureScript compiler will issue the following warning:

WARNING: Adding extern to Object for property wozz due to ambiguous expression (. z -wozz) ...

We need to add the return type information to the externs file:

var Foo = {};
/**
 * @constructor
 */
Foo.Bar = function() {};
/**
 * @return {Foo.Boo} <-- CHANGED
 */
Foo.Bar.prototype.baz = function() {};
/**
 * @constructor
 */
Foo.Boo = function() {};
Foo.Boo.prototype.woz = function() {};

Touching your source file and re-running build will result in a different warning:

WARNING: Cannot resolve property wozz for inferred type js/Foo.Boo in expression (. z -wozz)

As we can see the ClojureScript used the return type information to clarify the problem.

Original author: David Nolen