Understanding how to use Symbol.iterator in JavaScript/TypeScript

eye-catch JavaScript/TypeScript

While I looked for an interesting programming article, I found a programming exercise. Interestingly, Symbol.iterator is used in the answer. I’ve never used it in my work, so I tried to use it to learn how it works.

The exercise I tried is something like this.

Select an element randomly from an array ["One", "Two", "Three"]. If the order matches ["One", "Two", "Three"] repeated 3 times, attempt count and the actual result should be shown on the console.

How do you implement it?

Sponsored links

Implementation without Symbol.iterator

Firstly, let’s implement it without Symbol.iterator.

const ONE_TWO_THREE = [
    "One",
    "Two",
    "Three",
] as const;

type StringNumType = typeof ONE_TWO_THREE[number];

const ANSWER: StringNumType[] = [
    ...ONE_TWO_THREE,
    ...ONE_TWO_THREE,
    ...ONE_TWO_THREE,
];

function getNext(): StringNumType {
    return ONE_TWO_THREE[Math.floor(Math.random() * ONE_TWO_THREE.length)];
}

function func(): void {
    let attemptCount = 0;
    let answerIndex = 0;
    const actualStringArray: StringNumType[] = [];

    console.log("Start")
    while (true) {
        attemptCount++;

        const currentString = getNext();
        if (ANSWER[answerIndex] !== currentString) {
            actualStringArray.splice(0, actualStringArray.length);
            answerIndex = 0;
            continue;
        }

        actualStringArray.push(currentString);
        answerIndex++;
        if (answerIndex === ANSWER.length) {
            break;
        }
    }
    console.log(`attemp count: ${attemptCount}`);
    console.log(`Answer: ${actualStringArray.join(",")}`);
}

// An example result
// Start
// attemp count: 52189
// Answer: One,Two,Three,One,Two,Three,One,Two,Three

If you understand everything, you can skip to the next chapter. I will explain the code above from here.

const ONE_TWO_THREE = [
    "One",
    "Two",
    "Three",
] as const;

We need to prepare 3 strings. Even if const keyword is used, the array elements can be updated by using the method like push() and splice(). By adding as const, the variable becomes read-only and it’s impossible to change the element.

Without as const, the variable becomes an array and can be broken. This is important if you don’t want to have accidental change.

// "One" | "Two" | "Three"
type StringNumType = typeof ONE_TWO_THREE[number];

It creates a new type from an object (array). typeof keyword can create a new type from an object. By passing number to the array, it can extract the elements. I explained typeof in the following article.

const ANSWER: StringNumType[] = [
    ...ONE_TWO_THREE,
    ...ONE_TWO_THREE,
    ...ONE_TWO_THREE,
];

This array defines the desired output. It is ["One", "Two", "Three", "One", "Two", "Three", "One", "Two", "Three"]. Three dots are called Spread Operator. If you need a more detailed explanation, go to this article.

function getNext(): StringNumType {
    return ONE_TWO_THREE[Math.floor(Math.random() * ONE_TWO_THREE.length)];
}

This function returns the next value. Math.floor returns the largest integer less than or equal to a given number.

  • 0.3 -> 0
  • 1.44 -> 1
  • 0. 99 -> 0
  • 1.99 -> 1

Math.random returns 0 – 1 but we want 0 or 1. We need to return a value between 0 – 2, and thus, ONE_TWO_THREE.length is multiplied.

function func(): void {
    let attemptCount = 0;
    let answerIndex = 0;
    const actualStringArray: StringNumType[] = [];
    ...
}

It defines the main function and initializes the necessary variables.

while (true) {
    attemptCount++;
    const currentString = getNext();
    ...
}

When it gets the next value, the count must be incremented. The next value is assigned to currentString.

while (true) {
    attemptCount++;

    const currentString = getNext();
    if (ANSWER[answerIndex] !== currentString) {
        actualStringArray.splice(0, actualStringArray.length);
        answerIndex = 0;
        continue;
    }

    ...
}

It gets an expected value from ANSWER array and compare the two values. If the currentString doesn’t have an expected value, the two variables actualStringArray and answerIndex are initialized.

Since actualStringArray is const variable, it’s impossible to re-assign an empty array []. Thus, splice method is used here to clear the current elements.

By continue keyword, the program goes back to the top of the while loop.

while (true) {
    ...

    actualStringArray.push(currentString);
    answerIndex++;
    if (answerIndex === ANSWER.length) {
        break;
    }
}

If the currentString has an expected value, store the result and increment answerIndex so that it can extract the next expected value. If the answerIndex reaches the end of the array, it goes out from the while loop.

function func(): void {
    ...
    console.log(`attemp count: ${attemptCount}`);
    console.log(`Answer: ${actualStringArray.join(",")}`);
}

Then, output the result.

Sponsored links

Using Symbol.iterator for the initialization

Let’s see the second implementation.

// Same code from here -----
const ONE_TWO_THREE = [
    "One",
    "Two",
    "Three",
] as const;

type StringNumType = typeof ONE_TWO_THREE[number];

const ANSWER: StringNumType[] = [
    ...ONE_TWO_THREE,
    ...ONE_TWO_THREE,
    ...ONE_TWO_THREE,
];

function getNext(): StringNumType {
    return ONE_TWO_THREE[Math.floor(Math.random() * ONE_TWO_THREE.length)];
}
// Same code up here -----

function initializeIterator(): Iterator<StringNumType> {
    return ANSWER[Symbol.iterator]();
}

function func2(): void {
    let attemptCount = 0;
    const actualStringArray: StringNumType[] = [];

    let iterator = initializeIterator();

    console.log("Start")
    while (true) {
        attemptCount++;

        const nextAnswer = iterator.next();
        if (nextAnswer.done) {
            break;
        }

        const currentString = getNext();
        if (nextAnswer.value !== currentString) {
            iterator = initializeIterator();
            actualStringArray.splice(0, actualStringArray.length);
            continue;
        }

        actualStringArray.push(currentString);
    }
    console.log(`attemp count: ${attemptCount}`);
    console.log(`Answer: ${actualStringArray.join(",")}`);
}

I’ve never seen Symbol used in a real project. I had no idea how to use Symbol but this is one of the good examples.

function initializeIterator(): Iterator<StringNumType> {
    return ANSWER[Symbol.iterator]();
}

This function returns an object that implements iterator. A caller side can get the next value by calling iterator.next() method.

Symbol is a unique identifier. If we use a string as a key, there can be a collision if someone uses the same string. By using Symbol, we can avoid the case.

When a cursor is on Symbol.iterator, the following explanation is shown.

(property) SymbolConstructor.iterator: typeof Symbol.iterator
A method that returns the default iterator for an object. Called by the semantics of the for-of statement.

It is used in for-of.

function func2(): void {
    let attemptCount = 0;
    const actualStringArray: StringNumType[] = [];

    let iterator = initializeIterator();
    ...
}    

The first two lines are the same as before. The last statement gets an iterator object.

while (true) {
    attemptCount++;

    const nextAnswer = iterator.next();
    if (nextAnswer.done) {
        break;
    }
    ...

iterator.next() returns the next iterator result object. The object has done and value properties. If it doesn’t have the next value, done variable becomes true. It means the loop finishes.

const currentString = getNext();
if (nextAnswer.value !== currentString) {
    iterator = initializeIterator();
    actualStringArray.splice(0, actualStringArray.length);
    continue;
}

What it does is basically the same as the previous code. Compare the current value and the expected value. If it’s different, it initializes the variables.

iterator is also initialized so that the next iterator.next() call returns the first element of ANSWER variable.

How is Symbol.iterator is defined in the interface

Let’s see the initializeIterator function again.

function initializeIterator(): Iterator<StringNumType> {
    return ANSWER[Symbol.iterator]();
}

It looks a function call. Why do we need “()” at the end? If we check the interface, it is defined in the following way.

interface ReadonlyArray<T> {
    /** Iterator of values in the array. */
    [Symbol.iterator](): IterableIterator<T>;

It is NOT ANSWER[Symbol.iterator]. It must be ANSWER[Symbol.iterator](). It returns an iterable iterator and we can call it as many as we want when we want to go back to the first element.

By the way, Iterator is different from Iterable. They are defined in the following way.

interface Iterator<T, TReturn = any, TNext = undefined> {
    // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

interface IterableIterator<T> extends Iterator<T> {
    [Symbol.iterator](): IterableIterator<T>;
}

Iterable is an interface that returns an iterator.

ANSWER[Symbol.iterator]() returns an IterableIterator object. If Symbol.iterator is used again, it means that the same object is returned. See the following example.

const array = ["11", "22", "33", "44"];
let iterator = array[Symbol.iterator]();
console.log(iterator.next());  // { value: '11', done: false }
console.log(iterator.next());  // { value: '22', done: false }

let iterator2 = iterator[Symbol.iterator]();
console.log(iterator2.next()); // { value: '33', done: false }
console.log(iterator2.next()); // { value: '44', done: false }
console.log(iterator.next());  // { value: undefined, done: true }

It gets an iterator and call next() method twice. And then, it gets an IterableIterator object by calling iterator[Symbol.iterator](). next() method call for the second object returns the next object of the first iterator. Both variables have the same reference.

But if we get an object from array variable again, it returns to the top element again.

iterator = array[Symbol.iterator]();
console.log(iterator.next());  // { value: '11', done: false }

Conclusion

By using Symbol.iterator, we can extract an iterator object from an array.

iterator = array[Symbol.iterator]();
let iteratorResult = iterator.next();
while (!iteratorResult.done) {
    // do something with iteratorResult.value

    iteratorResult = iterator.next();
}

I didn’t have a good idea of how to use Symbol but this is a good example.

Which code do you prefer? I guess not many developers know Symbol.iterator and thus the second implementation is less readable. I didn’t understand how it worked at the first glance.

This technique is good to know but if we want to use it in a real project where there are several developers, we should have a dev-exchange meeting for it.

In this exercise, it doesn’t improve the maintainability very much. I prefer the first implementation.

Comments

Copied title and URL