AngularのSPA(Single Page Application)のプログラムを書いてたら、以下の問題に遭遇しました。
- TypeScriptのコード内でのsetInterval()の返り値には罠がある
- https://blog.kubosho.com/entry/setinterval-trap-on-typescript/
内容としては「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()」に変えると、戻り値は数値型で通用します。
- TypeScript - use correct version of setTimeout (node vs window)
- https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window
// 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.
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になっているようです。
「window.」の省略について
JavaScriptでは、グローバルオブジェクトの「window.」の記述を省略できます。
- 「window.location.href」を「location.href」と書けます
- 「window.alert()」は「alert()」と書けます
ただし私が遭遇した今回の事例は、まさに「window.」を省略したことによって起こりました。 調べてみると、確かにNode.jsにはTimerのAPIで「setTimeout()」関数があります。
- setTimeout(callback, delay[, ...args])
- https://nodejs.org/api/timers.html
「window.」を省略したことで、TypeScriptがNode.jsの「setTimeout()」を使うと判断したという理屈になります。
さいごに
色々と調べてみた結果ですが、私の中では以下の結論に落ち着きました。
- 確実にブラウザの「setTimeout()」を動かしたいなら、「window.」は省略しないほうが良さそう
- ブラウザとサーバーサイドの両方で動作を想定するなら、「window.」は省略して記述する