Skip to content

Instantly share code, notes, and snippets.

@swannodette
Last active August 7, 2023 16:13
Show Gist options
  • Save swannodette/4fc9ccc13f62c66456daf19c47692799 to your computer and use it in GitHub Desktop.
Save swannodette/4fc9ccc13f62c66456daf19c47692799 to your computer and use it in GitHub Desktop.
Externs Inference

Externs Inference

Integrating third party JavaScript libraries not written with Google Closure Compiler in mind continues to both be a source of error for users when going to production, and significant vigilance and effort for the the broader community (CLJSJS libraries must provide up-to-date and accurate externs).

In truth writing externs is far simpler than most users imagine. You only need externs for the parts of the library you actually intend to use from ClojureScript. However this isn't so easy to determine from Closure's own documentation. Still in the process of writing your code it's easy to miss a case. In production you will see the much dreaded error that some mangled name does not exist. Fortunately it's possible to enable some compiler flags :pretty-print true :pseudo-names true to generate an advanced build with human readable names. However debugging missing externs means compiling your production build for each missed case. So much time wasted for such simple mistakes damages our sense of productivity.

If we squint a bit perhaps we can see how this issue isn't quite so different from the one of reflection in Clojure JVM. When writing high performance code it's easy to miss a needed type hint and get orders of magnitude worse performance. In order to help the user easily locate the issue, Clojure has long had a per-file level dynamic var *warn-on-reflection* which allows users to easily locate forms where the compiler could not resolve the type.

(set! *warn-on-reflection* true)
(defn foo [x]
  (.indexOf x "bar"))
;;  Reflection warning - call to method indexOf can't be resolved (target class is unknown).

All you need to do is add a type hint and the warning goes away and the compiler will generate optimal bytecode:

(set! *warn-on-reflection* true)
(defn foo [^String x]
  (.indexOf x "bar"))

But this is precisely what interop into third party JavaScript looks like, only the performance concern is irrelevant. Consider if we had a file level warning for interop calls where we can't resolve the type.

(set! *warn-on-infer* true)
(defn foo [c]
  (.render c))
;; WARNING: Cannot infer target type for (. c render) at line ...

Again all we need to do is add a type hint and the warning goes away:

(set! *warn-on-infer* true)
(defn foo [^js/React.Component c]
  (.render c))

In this case we didn't make the code 100X faster, instead we now have enough information to automatically generate the extern for you:

var React;
React.Component;
React.Component.prototype.render;

This isn't just a thought experiment. As of today we have experimental support for the above in ClojureScript master. Simply add a new compiler option :infer-externs true to your compiler config. Now when you build your project you will get a new file in your :output-dir named inferred_externs.js. When you do an advanced build, this externs file will be used.

Please give it a spin and report issues and ideas for further enhancement!

I'd like to thank Maria Geller in particular, this feature is based on her Google Summer of Code work.

@xlisp
Copy link

xlisp commented Dec 18, 2016

+1

@comprehendreality
Copy link

A bit confused, to make sure I understand... I can use warn-on-infer during development to help me understand where I need to use the type hints, once I have all the type hints, advanced compilation will generate the inferred_externs.js file?

@skrat
Copy link

skrat commented Dec 18, 2016

So do we need to use (set! *warn-on-infer* true) to use the :infer-externs? Or do we need it only in during dev time?

@agzam
Copy link

agzam commented Dec 20, 2016

how would on use master dependency of cljs? 1.9-SNAPSHOT doesn't seem to work...

@kommen
Copy link

kommen commented Dec 21, 2016

@agzam until a new ClojureScript build is released I think you have to compile it yourself from the source. It worked for me and was seamless. See https://github.com/clojure/clojurescript/wiki/Building-the-compiler

In case somebody want's to use this with figwheel, be sure to use the latest version of lein-figwheel, see bhauman/lein-figwheel#507

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment