DEV Community

Cover image for JS Magic: Making a Range Syntax for Numbers
Jon Randy 🎖️
Jon Randy 🎖️

Posted on • Updated on

JS Magic: Making a Range Syntax for Numbers

WARNING: using this technique in production code may result in injury or death during code reviews. Use at your own risk.

I've seen the following code around a few times to 'spread' a number in JS:

Number.prototype[Symbol.iterator] = function *range() {
  for (let i = 0; i <= this; i++) {
    yield i;
  }
}
const numbers = [...3]
console.log(numbers); // [0, 1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

(If you don't understand what's going on here, we're making an object into an iterable by giving it an iterator method - MDN explains it pretty well)

This is kind of cool I guess, but the range always starts from a fixed value (0 in this case) - which is kind of a pain. Wouldn't it be great if we could take it up a notch and do this:

console.log([3...7])
Enter fullscreen mode Exit fullscreen mode

Well, tough s**t, we can't. So, how about this?

console.log(3[...7])
Enter fullscreen mode Exit fullscreen mode

Again, nope - Uncaught SyntaxError: expected expression, got '...'.

Not quite so pretty, but how about this?

console.log(3[[...7]])
Enter fullscreen mode Exit fullscreen mode

This gives us another error: Uncaught TypeError: 7 is not iterable - but, as we saw above we CAN make numbers iterable, and running the above code after first doing that results in... no errors, and 'undefined' logged in the console. It appears we may have a route to a solution...

Long story short - I dreamed up the following solution (yes, I'm not normal). It combines making a number iterable (but not in the same way as above), with some ideas from Metho, Metho-Number and Turboprop. Gold stars to everyone who follows how it works ⭐:

// When we 'spread' a number, set up a temporary method on the Number
// prototype that will return our final result, and get the Symbol
// that is the 'name' of this method
Number.prototype[Symbol.iterator] = function *range() {
  yield attachTempNumRangeMethod(+this)
}

// Create a preconfigured range method, and attach it to the Number
// prototype (idea from Metho)
function attachTempNumRangeMethod(rangeEnd) {
  const s = Symbol()
  Object.defineProperty(
    Number.prototype, s,
    { configurable: true, get: makeRangeMethod(rangeEnd, s) }
  )
  return s
}

// Create a function to count from or to the given end value, then
// delete itself from the Number prototype (idea from Metho-number)
function makeRangeMethod(end, sym) {
  return function range() {
    const step = this<=end ? 1 : -1
    let arr = [], i, d = end>this
    for (i=+this; d ? i<=end : i>=end; i+=step) arr.push(i)
    delete Number.prototype[sym]
    return arr
  }
}

// Set up a 'toPrimitive' method on the Array prototype so we can
// hijack it if we spot an array containing a Number method symbol
// we previously set up (idea from Turboprop)
Array.prototype[Symbol.toPrimitive] = function (hint) {
  // this is dangerous, so try to and leave all default behaviour alone:
  if (hint === 'default') return this.toString()
  if (hint === 'number') return Number(this.toString())
  if (this.length != 1 && !Number.prototype.hasOwnProperty(this[0]))
    return this.toString()
  // return our symbol that names the Number method
  return this[0]
}
Enter fullscreen mode Exit fullscreen mode

Still with me ? Let's test it:

37[[...42]].forEach(n => console.log(n))
// 37
// 38
// 39
// 40
// 41
// 42

console.log(10[[...1]])
// Array(10) [ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ]
Enter fullscreen mode Exit fullscreen mode

TA-DA 🎉

Tune in again next time for more JavaScript syntax abuse!

written by human


Further reading

Top comments (1)

Collapse
 
jonrandy profile image
Jon Randy 🎖️

Update: I generalised this technique into a library allowing you to add a range syntax to any object: