Functions returning functions returning functions can begin to look a bit unwieldy. The arrow function has helped the syntax a lot, but maybe using a curry utility function to combine all the arguments into a single function will help your syntax look a little cleaner. Currying isn't necessary for our pattern, but you'll definitely see it used in many patterns involving functions returning functions and maybe you'll grow to love it.
export let createInterval = curry((time, listener) => { let i = 0 let id = setInterval(() => { listener(i++) }, time) return () => { clearInterval(id) } }) export let addListener = curry((selector, eventType, listener) => { let element = document.querySelector(selector) element.addEventListener(eventType, listener) return () => { element.removeEventListener(eventType, listener) } }) let zip = curry((broadcaster1, broadcaster2, listener) => { let buffer1 = [] let cancel1 = broadcaster1(value => { buffer1.push(value) if (buffer2.length) { listener([buffer1.shift(), buffer2.shift()]) } }) let buffer2 = [] let cancel2 = broadcaster2(value => { buffer2.push(value) if (buffer1.length) { listener([buffer1.shift(), buffer2.shift()]) } }) return () => { cancel1() cancel2() } }) let clickAndTick = zip( addListener("#button", "click"), createInterval(1000) ) let cancelClickAndTick = clickAndTick(value => { console.log(value) })