Welcome! Please see the About page for a little more info on how this works.

0 votes
in ClojureScript by

This started as a discussion with David Nolen on Slack at https://clojurians.slack.com/archives/C07UQ678E/p1644422895167709

It seems to be a common pattern nowadays to rely on JS interop syntax along with ^js at places where the compiler warns the user with "Cannot infer target type". At the same time, it doesn't seem to be uncommon to use built-in CLJS functions and macros like first, seq, when-first, etc. on JS objects and arrays - after all, the support for them is explicitly there.

This leads to a problem that makes it incredibly easy to introduce issues to an optimized build that are impossible to guard against, unless you put ^js everywhere you do JS interop.

It is especially true in really interop-heavy code, like this one, for example:

(defn replace-track-source! [^js playlist track-name src]
  (loop [tracks (.-tracks playlist)]
    (when-first [^js track tracks]
      (if (= (.-name track) track-name)
        (-> (to-audio-buffer src playlist)
            (.then (fn [audio-buffer]
                     (let [audio-buffer (audio-util/normalize-volume audio-buffer)]
                       (set! track -src audio-buffer)
                       (.setBuffer track audio-buffer)
                       (.setCues track 0 (.-duration audio-buffer))
                       (.setPlayout track (doto (Playout. (.-ac playlist) audio-buffer)
                                            (.setVolumeGainLevel (.-gain track))
                                            (.setStereoPanValue (.-stereoPan track))))
                       (.calculatePeaks track (.-samplesPerPixel playlist) (.-sampleRate playlist))
                       (.adjustDuration playlist)
                       (.drawRequest playlist)))))
        (recur (next tracks))))))

This case above is problematic because the compiler doesn't warn you if you omit the ^js in front of the track binding. But without that tag, names of all the functions called on track will be minified.

One potential solution to this is to introduce an opt-in warning when a CLJS function that doesn't preserve the ^js tag is used on a JS object/array.

A minimal reproducible example that I tried with CLJS 1.11.60 and `clj -M -m cljs.main --optimizations advanced -c app.core`:

    (ns app.core)
    (set! *warn-on-infer* true)
    (defn call-baz [x]
      ;; Emits a warning.
      (.baz x))
    (defn do-stuff [^js items]
      (when-first [item items]
        (js/console.log "about to call doStuff")
        ;; Does not emit a warning, gets minified.
        (.doStuff item)))
    (do-stuff #js [#js {:doStuff #(js/console.log "doing stuff")}])

1 Answer

0 votes