[キャッチアップ] setTimeout と window と globalThis
概要
ある日、 setTimeout
を使った以下のような JavaScript コードを TypeScript に置き換えようとした。(※ 以下コードは冗長だがサンプルとして改変している)
function startTimer(callback, millisec) {
const timerId = setTimeout(() => {
callback();
}, millisec);
return timerId;
}
setTimeout
の返り値といったら、 number
だろうと、以下のように片付けを行ったところ、エラーが発生していることに気づいた。
function startTimer(callback, millisec) {
const timerId: number = setTimeout(() => {
callback();
}, millisec);
return timerId
}
Type 'Timeout' is not assignable to type 'number'.ts(2322)
エディタ上で確認できる型推論によると、 setTimeout
の返り値は number
ではなく、 NodeJS.Timeout
になるらしい。
function startTimer(callback, millisec) {
const timerId: NodeJS.Timeout = setTimeout(() => {
callback();
}, millisec);
return timerId;
}
この型は一体何者なのか、 Node
と書いてあるが、ブラウザで動かす場合に問題はないのか?
わからないことばかりだったので、詳しく調べてみることにした。
関連記事を調べる
同じ疑問に辿り着く先人は多数いたようで、以下の記事が非常に参考になった。
TypeScriptでsetTimeout()がNodeJS.Timerになる理由から、window.setTimeout()との違いを理解する
既によくまとまった内容だが、さらに知りたいポイントだけまとめると以下ようになる
-
window.setTimeout
と明記することで、戻り値は想定通りにnumber
となる -
window.setTimeout
は、ブラウザが用意しているsetTimeout
を明示的に使うことができる - Node における
setTimeout
は、ブラウザ外のJSや、自分たちで用意した関数も考慮される - 確実にブラウザの
setTimeout
を動かす場合は、window
を省略しないほうが良さそう
どうやらブラウザにおける window
の省略と、 Node によるブラウザ互換のAPIが噛み合って起こっている事象らしい。
そもそも window ってなんだっけ
Window - Web API | MDN によると、 window
オブジェクト は以下のようなものとされている
- スクリプトを実行しているウィンドウを表すグローバル変数
- ウィンドウの概念とは必ずしも直結しない、関数/名前空間/オブジェクト/コンストラクタのホームとなっている
- ブラウザ内の JavaScript は、
window
がグローバルオブジェクトとなり、全てのコードはwindow
のコンテキスト内で実行される
ブラウザコンソールを開いて、 this
を実行すると window
オブジェクトが取得できるのがそういうこと。
this
Window {0: global, 1: Window, window: Window, self: Window, document: document, name: "", location: Location, …}
なら window.setTimeout
のように、 window
を明示できるのは何故なのかと言うと、 Window#window
が定義されているからだった。
setTimeout
は何なのか
Node における
を見比べてみると、ブラウザにおける setTimeout
と、Node における setTimeout
には以下のような違いがあった。
ブラウザ版は、実行するコードを文字列で渡すことができるが、Node 版はできない
ブラウザ
setTimeout('alert("OK")', 1000)
7115
Node
> setTimeout('alert("OK")', 1000)
Uncaught:
TypeError [ERR_INVALID_CALLBACK]: Callback must be a function. Received 'alert("OK")'
ブラウザ版の返り値は数値 で、 Node 版はオブジェクト
ブラウザ
setTimeout(() => {}, 1000)
7451
Node
> setTimeout(() => {}, 1000)
Timeout {
_idleTimeout: 1000,
_idlePrev: [TimersList],
_idleNext: [TimersList],
_idleStart: 128598,
_onTimeout: [Function],
_timerArgs: undefined,
_repeat: null,
_destroyed: false,
[Symbol(refed)]: true,
[Symbol(asyncId)]: 133,
[Symbol(triggerId)]: 5
}
Node 版は Timer オブジェクト経由でタイマーをリフレッシュできる
ブラウザ
const timerId = setTimeout(() => {}, 5000)
clearTimeout(timerId)
Node
const timer = setTimeout(() => {}, 5000)
timer.refresh()
TypeScript ではどう型を付けるべきか
これまでのように、ブラウザでコードが実行されることを考えると、 window
を省略して setTimeout
を書きたくなるものだが、TypeScript コンパイラは Node の世界で動く以上、 setTimeout
と書いてしまうと Node.Timeout
と推論されてしまう。
エディタの補完に従って、以下のようなコードを書いてしまうと、コンパイルは正常に行われるが、いざブラウザで実行すると、以下のようなエラーが出てしまう。
function startTimer(callback, millisec) {
return setTimeout(() => {
callback();
}, millisec);
}
const timer = startTimer(() => {}, 1000)
timer.refresh()
VM607:1 Uncaught TypeError: timer.refresh is not a function
at <anonymous>:1:3
上記のような Node.Timeout
特有の機能を使わなければこれでも問題ないが、やはり安全策として window
を明記するようにし、不可能な参照はコンパイル段階で弾けるようにしておきたい。
function startTimer(callback, millisec) {
return window.setTimeout(() => {
callback();
}, millisec);
}
const timer = startTimer(() => {}, 1000)
timer.refresh() // Property 'refresh' does not exist on type 'number'.
globalThis について
ブラウザスコープでのみ window
がグローバルオブジェクトになると言った、JavaScript の実行環境ごとにグローバルオブジェクトが異なる問題を調べていると登場するのが globalThis
だ。 globalThis
は ES2020 で追加された新機能になる。
まずは以下辺りに目を通してどんなものかを調べてみた。
- globalThis - JavaScript | MDN
- JavaScriptの次の仕様ES2020で追加されることが決定した新機能まとめ - ICS MEDIA
- javascript - JavascriptのglobalThisとは何ですか?これの理想的なユースケースは何ですか? - ITツールウェブ
- 👻globalThis👻と🌏global🌏と🌝this🌝 - Qiita
わかったことは以下の通り
- グローバルオブジェクトは処理系によって大きく異る
- ブラウザは
window
- Node は
global
- WebWorker は `self
- ブラウザは
- 同じコードでもそれぞれのグローバルオブジェクトを参照できるのが
globalThis
- IE11 はサポートされていない
ちなみに IE11 でも corejs を用いた Polifill が適用可能なので、 babel なりのトランスパイルをちゃんと組んでいれば IE11 をサポートするコードでも使用可能なようです。
globalThis と TypeScript
では処理系によって参照するグローバルオブジェクト切り替えるための globalThis
を、 TypeScript で利用した場合にその型はどうなるのだろうか。
globalThis を参照すると、型は globalThis
モジュールとして推論される
const _this = globalThis
また Node のグローバルオブジェクトである global
の型は NodeJS.Global & typeof globalThis
に、 ブラウザのグローバルオブジェクトである window
は Window & typeof globalThis
となっている。
const _global = global // NodeJS.Global & typeof globalThis
const _window = window // Window & typeof globalThis
このことから、 TypeScript は Node 及び ブラウザ両処理系のグローバルオブジェクトの型をサポートしており、 typeof globalThis
との Intersect によって、実行環境で利用可能なプロパティのみのインタフェースが提供できるようになっていることがわかる。
(この辺りはちょっと理解が怪しい)