TypeScriptでsetTimeout()がNodeJS.Timerになる理由から、window.setTimeout()との違いを理解する

AngularのSPA(Single Page Application)のプログラムを書いてたら、以下の問題に遭遇しました。

内容としては「Type 'Timer' is not assignable to type 'number'.」というエラーメッセージが表示されます。 VSCode(Visual Studio Code)で関数の戻り値を確認すると、戻り値の型はNodeJS.Timerになってます。

私の場合は、Angularで「setTimeout()」を記述しただけです。なぜそれが「NodeJS.Timer」となってしまうのか気になったので、調べてみました。

window.setTimeout()にするとnumberになる

次のstack overflowを見るとわかるのですが、「setTimeout()」を「window.setTimeout()」に変えると、戻り値は数値型で通用します。

// Another workaround that does not affect variable declaration:
let n: number;
n = <any>setTimeout(function () { /* snip */  }, 500);

// Also, it should be possible to use the window object explicitly without any:
let n: number;
n = window.setTimeout(function () { /* snip */  }, 500);

この仕様はMDNにも記載されており、「window.setTimeout()」のドキュメントを確認すると、確かに戻り値は数値型です。

戻り値 timeoutID は、setTimeout() を呼び出して作成したタイマーを識別する正の整数値です。

引用:https://developer.mozilla.org/ja/docs/Web/API/WindowTimers/setTimeout

setTimeout()とwindow.setTimeout()の違い

ではなぜ「window.setTimeout()」にするとTypeScriptの戻り値の解釈が変わるのかと申しますと、これは「setTimeout()」との違いが関係するようです。

For JavaScript that does not run in a browser, the window object is not defined, so window.setTimeout() will fail. setTimeout() however, will work.

引用: https://stackoverflow.com/questions/20420429/what-the-difference-between-window-settimeout-and-settimeout

function myF() {
  function setTimeout(callback,seconds) {
    // call the native setTimeout function
    return window.setTimeout(callback,seconds*1000);
  }
  // call your own setTimeout function (with seconds instead of milliseconds)
  setTimeout(function() {console.log("hi"); },3);
}
myF();

要約すると「window.setTimeout()」が明示的にブラウザの「setTimeout()」を使うよう指示していることに対し、 単なる「setTimeout()」ではブラウザ外(例えばNode.jsのサーバーサイド)のJSや、自分たちで用意した関数も考慮されます。

その結果「setTimeout()」のTypeScriptにおける戻り値の型が、NodeJS.Timerになっているようです。

f:id:konosumi:20190526144410j:plain

「window.」の省略について

JavaScriptでは、グローバルオブジェクトの「window.」の記述を省略できます。

  • 「window.location.href」を「location.href」と書けます
  • 「window.alert()」は「alert()」と書けます

ただし私が遭遇した今回の事例は、まさに「window.」を省略したことによって起こりました。 調べてみると、確かにNode.jsにはTimerのAPIで「setTimeout()」関数があります。

「window.」を省略したことで、TypeScriptがNode.jsの「setTimeout()」を使うと判断したという理屈になります。

さいごに

色々と調べてみた結果ですが、私の中では以下の結論に落ち着きました。

  • 確実にブラウザの「setTimeout()」を動かしたいなら、「window.」は省略しないほうが良さそう
  • ブラウザとサーバーサイドの両方で動作を想定するなら、「window.」は省略して記述する