Iterator.prototype.join

(This was previously brought up in tc39/proposal-iterator-helpers#294, where the last comment suggested that the discussion be continued here.)

This function would join values produced by an iterator into a single string, like Array.prototype.join does for array-likes. Several other popular languages provide this feature, including Java, Kotlin, .NET and Python. Compare it to existing solutions in JS:

  • .toArray().join() has to collect all values into an intermediate array, which may use a lot of memory.
  • .reduce((acc, cur) => `${acc},${cur}`, "").slice(1) is quite verbose.

Below are preliminary spec text and a polyfill.


Iterator.prototype.join ( separator )

This method performs the following steps when called:

  1. Let O be the this value.
  2. If O is not an Object, throw a TypeError exception.
  3. Let iterated be the Iterator Record { [[Iterator]]: O, [[NextMethod]]: undefined, [[Done]]: false }.
  4. If separator is undefined, let sep be ",".
  5. Else,
    1. Let sep be Completion(ToString(separator)).
    2. IfAbruptCloseIterator(sep, iterated).
  6. Set iterated to ? GetIteratorDirect(O).
  7. Let result be the empty String.
  8. Let counter be 0.
  9. Repeat,
    1. Let value be ? IteratorStepValue(iterated).
    2. If value is DONE, return result.
    3. If counter > 0, set result to the string-concatenation of result and sep.
    4. If value is neither undefined nor null, then
      1. Let string be Completion(ToString(value)).
      2. IfAbruptCloseIterator(string, iterated).
      3. Set result to the string-concatenation of result and string.
    5. Set counter to counter + 1.

const { Object, Symbol, TypeError } = globalThis;
Object.defineProperty(Iterator.prototype, "join", {
  value: {
    join(separator) {
      if (Object(this) !== this) {
        throw new TypeError("Iterator.prototype.join called on non-object");
      }
      let sep = ",";
      if (separator !== undefined) {
        try {
          sep = `${separator}`;
        } catch (e) {
          try {
            this.return?.();
          } catch {}
          throw e;
        }
      }
      let result = "";
      let counter = 0;
      for (const value of { [Symbol.iterator]: () => this }) {
        if (counter > 0) {
          result += sep;
        }
        if (value !== undefined && value !== null) {
          result += `${value}`;
        }
        counter++;
      }
      return result;
    },
  }.join,
  writable: true,
  configurable: true,
});
3 Likes

See https://github.com/tc39/proposal-iterator-sequencing

That's not the same. The method here is analogous to Array.prototype.join not .concat

I think this is a reasonable proposal. Can you collect some use cases? Real-world examples you can find of people using alternative approaches like the ones you list would be even better.

GitHub code search ( Code search results · GitHub ) turns up a fair number of results. There’s a lot of false positives in that search, but also some true positives.

Searching specifically for [...someMap.keys()].join() and Array.from(someMap.keys()).join() gives 2.3k results combined.

1 Like

There’s also the broader set of Python use-cases, where .join() is a method on strings instead and takes the iterable as an argument. It’s barely even a consideration whether you’re handling a real list or an iterator/generator, they’ll both just join to strings no problem.

Couldn’t sleep so I wrote up the proposal and put it on the agenda for the November meeting.

1 Like