Search code examples
javascriptnode.jstypescriptasynchronousiterator

TS2488: Type 'string | null' must have a '[Symbol.iterator]()' method that returns an iterator


I am trying to convert https://github.com/camwiegert/typical/blob/master/typical.js into TypeScript.

In Vanilla JS, it looks like:

typical.js

export async function type(node, ...args) {
    for (const arg of args) {
        switch (typeof arg) {
            case 'string':
                await edit(node, arg);
                break;
            case 'number':
                await wait(arg);
                break;
            case 'function':
                await arg(node, ...args);
                break;
            default:
                await arg;
        }
    }
}

async function edit(node, text) {
    const overlap = getOverlap(node.textContent, text);
    await perform(node, [...deleter(node.textContent, overlap), ...writer(text, overlap)]);
}

async function wait(ms) {
    await new Promise(resolve => setTimeout(resolve, ms));
}

async function perform(node, edits, speed = 60) {
    for (const op of editor(edits)) {
        op(node);
        await wait(speed + speed * (Math.random() - 0.5));
    }
}

export function* editor(edits) {
    for (const edit of edits) {
        yield (node) => requestAnimationFrame(() => node.textContent = edit);
    }
}

export function* writer([...text], startIndex = 0, endIndex = text.length) {
    while (startIndex < endIndex) {
        yield text.slice(0, ++startIndex).join('');
    }
}

export function* deleter([...text], startIndex = 0, endIndex = text.length) {
    while (endIndex > startIndex) {
        yield text.slice(0, --endIndex).join('');
    }
}

export function getOverlap(start, [...end]) {
    return [...start, NaN].findIndex((char, i) => end[i] !== char);
}

And my TS conversion looks like:

typical.ts

export async function type(node: HTMLElement, ...args: any[]): Promise<void> {
  for (const arg of args) {
    switch (typeof arg) {
      case 'string':
        await edit(node, arg);
        break;
      case 'number':
        await wait(arg);
        break;
      case 'function':
        await arg(node, ...args);
        break;
      default:
        await arg;
    }
  }
}

async function edit(node: HTMLElement, text: string): Promise<void> {
  const overlap = getOverlap(node.textContent, text);
  await perform(node, [
    ...deleter(node.textContent, overlap),
    ...writer(text, overlap),
  ]);
}

async function wait(ms: number): Promise<void> {
  await new Promise(resolve => setTimeout(resolve, ms));
}

async function perform(
  node: HTMLElement,
  edits: Iterable<string | null>,
  speed: number = 60
): Promise<void> {
  for (const op of editor(edits)) {
    op(node);
    await wait(speed + speed * (Math.random() - 0.5));
  }
}

export function* editor(
  edits: Iterable<string | null>
): Generator<(node: any) => number, void, unknown> {
  for (const edit of edits) {
    yield node => requestAnimationFrame(() => (node.textContent = edit));
  }
}

export function* writer(
  [...text]: string,
  startIndex: number = 0,
  endIndex: number = text.length
): Generator<string, void, unknown> {
  while (startIndex < endIndex) {
    yield text.slice(0, ++startIndex).join('');
  }
}

export function* deleter(
  [...text]: string | null,
  startIndex: number = 0,
  endIndex: number = text.length
): Generator<string, void, unknown> {
  while (endIndex > startIndex) {
    yield text.slice(0, --endIndex).join('');
  }
}

export function getOverlap(start: any, [...end]: Iterable<any>): number {
  return [...start, NaN].findIndex((char, i) => end[i] !== char);
}

Mostly, I followed VSCode's advice on hover to type it & some logic.

However, it gives me an error saying:

typical.ts(61,3): semantic error TS2488: Type 'string | null' must have a 'Symbol.iterator' method that returns an iterator.

Line 61 is deleter() function's [...text], i.e,:

[...text]: string | null,

How do I solve this?


Solution

  • So, [...text] argument from function deleter is actually an array of characters. for example, consider this call deleter(node.textContent, overlap). type of node.textContent can be string or null and the argument type is [...text]: string | null but textContent can be null and you cannot destructure it to [...text]. There will be two paths.

    1. [...text] = node.textContent
    2. [...text] = null

    The second one is invalid that's why you got this type error. You may change the function to

    export function* deleter(
      text: string | null,
      startIndex: number = 0
    ): Generator<string, void, unknown> {
      if (text === null) {
        yield ""// Or throw
        return
      }
      const [...chars] = text
      let endIndex = chars.length
      while (endIndex > startIndex) {
        yield chars.slice(0, --endIndex).join("");
      }
    }
    

    Update 1:
    Path was expected to have a container! error can be resolved by adding curly braces {...} around the switch case body. Like.

    case 'string':
    {
      await edit(node, arg);
    }
    break;
    

    unknown Statement of type "ForOfStatement" error can be resolve by adding correct babel configs. Here is the correct config.

    //.babelrc.js
    module.exports = {
      "presets": [
        [
          "@babel/env",
          {
            "targets": {
              "browsers": "last 2 Chrome versions",
              "node": "current"
            }
          }
        ]
      ],
      "plugins": [
        "@babel/plugin-transform-for-of",
        ['@babel/plugin-transform-runtime',
          {
            helpers: false
          }
        ],
        "@babel/plugin-transform-destructuring"
      ]
    }