投稿日:2022年1月26日
TypeScriptのクラスではコンストラクタの処理を非同期にすることができません。しかし、どうしても非同期処理を入れたいとき、何らかの対応をする必要があります。いくつかの方法があるので、自身で気に入った方法を選んでください。
この記事の内容は動画化されています。
TypeScriptでは仕様上、クラスのコンストラクタ(constructor)の処理を非同期にすることができません。
しかし、どうしても非同期処理を入れたいようなパターンが出てくることもあります。
そのような場合の対応方法はいくつかあります。
この記事では、その対応方法について詳しく解説します。
この記事のコードは以下の環境で確認されました。
以下がサンプルコードになります
https://github.com/ogipochi/async_constructor_ts_sample
はじめにコンストラクタ(constructor)に非同期の処理が入った場合、どのような挙動をするのかを見てみましょう。
ここでは非同期の処理のサンプルとしてブラウザAPIのGeoLocationAPIを使ってこんなクラスを作成してみます。
/**
* GeoLocationAPIを利用して緯度・軽度の情報を取得して
* 位置情報を扱うクラス
*/
export class MyGeolocationData {
// 緯度
private latitude: number;
// 軽度
private longitude: number;
/**
* コンストラクタ
* 現在の緯度と軽度を取得してプロパティにセットして
* 現在の緯度経度をコンソールに出力
*/
constructor(){
navigator.geolocation.getCurrentPosition((position) => {
this.latitude = position.coords.latitude;
this.longitude = position.coords.longitude;
this.logInfo("INIT");
});
}
/**
* 指定された緯度・軽度分移動した後、現在の緯度軽度をコンソールに出力
* @param addedLongitude
* @param addedLatitude
*/
move = (addedLongitude: number, addedLatitude: number) => {
this.longitude += addedLongitude;
this.latitude += addedLatitude;
this.logInfo("MOVE");
}
/**
* 情報をコンソールログに出力する
*/
logInfo = (signature: string) => {
console.log(`【${signature}】lng = ${this.longitude}, lat = ${this.latitude}`);
}
}
このクラスは問題ないように見えます(move()のしきい値処理をしていない点については簡単のために多目に見てください)。
ここで以下のようにクラスのインスタンス化と関数呼び出しを行うとどのような結果になるでしょうか?
// [...]
const myGeolocationData = new MyGeolocationData();
myGeolocationData.move(2,10);
結果は以下の通りです。
【MOVE】lng = NaN, lat = NaN
【INIT】lng = 137.7324108, lat = 34.668629
おや??
想定していた結果ではありません。
これはコンストラクタで呼び出しているgetCurrentPosition()が非同期処理であることが原因です。コンストラクタでのプロパティの初期化が終わる前にmove()が呼び出されてしまったために、move()の処理が以下のような処理になってしまったのです。
undefined += addedLongitude; // NaNになる
undefined += addedLatitude; // NaNになる
ではこれを解決する方法を見ていきましょう。
はじめに最も単純な方法から考えて見ましょう。
非同期の初期化処理を別関数に分けて、使用時にその関数を呼び出して上げる方法です。
以下がそれを実践したサンプルです。
/**
* GeoLocationAPIを利用して緯度・軽度の情報を取得して
* 位置情報を扱うクラス
*/
export class MyGeolocationData {
// 緯度
private latitude: number;
// 軽度
private longitude: number;
/**
* コンストラクタ
* 現在の緯度と軽度を取得してプロパティにセットして
* 現在の緯度経度をコンソールに出力
*/
constructor(){
}
/**
* 初期化処理をPromiseで処理
* @returns
*/
init = () => {
return new Promise<void>((resolve, reject) => {
navigator.geolocation.getCurrentPosition((position) => {
this.latitude = position.coords.latitude;
this.longitude = position.coords.longitude;
this.logInfo("INIT");
resolve();
},(err) =>{
reject(err);
});
});
}
// [...]
}
ここでgetCurrentPosition()をPromiseでラッピングしている点に注意してください。
このように実装することで、呼び出し時にcallbackを渡す必要がなくなりよりシンプルになります(詳しくはコールバック地獄で調べてみてください)。
これは以下のように呼び出すことができます。
const myGeolocationData = new MyGeolocationData();
// init()での初期化が完了したらmove()を呼び出す
myGeolocationData.init().then(() => {
myGeolocationData.move(2,10);
});
しかし、この解決方法で一つ問題点があります。
それは、呼び出し方法を間違えると内部のプロパティが間違った値になり、正しい動作をしない点です。
例えば以下のような初期化処理をしていない呼び出し方をしても、問題なくコンパイルが通ってしまいます。
const myGeolocationData = new MyGeolocationData();
// init()を呼び忘れている
myGeolocationData.move(2,10);
次の解決法ではこの点に焦点を当ててみます。
この解決法では以下の対応をします。
この対応を入れると以下のようになります。
/**
* GeoLocationAPIを利用して緯度・軽度の情報を取得して
* 位置情報を扱うクラス
*/
export class MyGeolocationData {
// 緯度
private latitude: number;
// 軽度
private longitude: number;
/**
* コンストラクタ
* 現在の緯度と軽度を取得してプロパティにセットして
* 現在の緯度経度をコンソールに出力
* このコンストラクタを直接呼ぶことはできない
*
*/
private constructor(_latitude: number, _longitude: number){
this.latitude = _latitude;
this.longitude = _longitude;
this.logInfo("INIT");
}
/**
* 初期化処理をPromiseで処理
* このinit()を通してでしかインスタンス化できない
* @returns
*/
static init = () => {
return new Promise<MyGeolocationData>((resolve, reject) => {
navigator.geolocation.getCurrentPosition((position) => {
const myGeolocationData = new MyGeolocationData(position.coords.latitude, position.coords.longitude);
resolve(myGeolocationData);
},(err) =>{
reject(err);
});
});
}
// [...]
}
これは以下のように使用することができます。
MyGeolocationData.init().then(myGeolocationData => {
myGeolocationData.move(2,10)
});
今回の場合、init()を呼び出すことでしかインスタンス化はできないため、概ね問題なさそうですね!
いいじゃん
React使ってると時々必要になるんだよね〜、コンストラクタに非同期処理入れる方法って