※このブログではサーバー運用、技術の検証等の費用のため広告をいれています。
記事が見づらいなどの問題がありましたらContactからお知らせください。


【TypeScript】constructorで非同期処理を呼び出したい場合の解決方法まとめ

TypeScript 非同期処理 class constructor Promise 非同期

投稿日:2022年1月26日

このエントリーをはてなブックマークに追加
TypeScriptのクラスではコンストラクタの処理を非同期にすることができません。しかし、どうしても非同期処理を入れたいとき、何らかの対応をする必要があります。いくつかの方法があるので、自身で気に入った方法を選んでください。

はじめに

この記事の内容は動画化されています。

この記事について

TypeScriptでは仕様上、クラスのコンストラクタ(constructor)の処理を非同期にすることができません。
しかし、どうしても非同期処理を入れたいようなパターンが出てくることもあります。
そのような場合の対応方法はいくつかあります。
この記事では、その対応方法について詳しく解説します。

環境について

この記事のコードは以下の環境で確認されました。

  • node version 16.13.2
  • npm version 8.1.2
  • typescript version 4.5.5

参考

以下がサンプルコードになります

https://github.com/ogipochi/async_constructor_ts_sample


実践

コンストラクタに非同期の処理が入ると?

はじめにコンストラクタ(constructor)に非同期の処理が入った場合、どのような挙動をするのかを見てみましょう。

ここでは非同期の処理のサンプルとしてブラウザAPIのGeoLocationAPIを使ってこんなクラスを作成してみます。

index.ts
/**
 * 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()のしきい値処理をしていない点については簡単のために多目に見てください)。


ここで以下のようにクラスのインスタンス化と関数呼び出しを行うとどのような結果になるでしょうか?

index.ts
// [...]

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になる

ではこれを解決する方法を見ていきましょう。

解決法1:初期化処理を別のメソッドに分ける

はじめに最も単純な方法から考えて見ましょう。

非同期の初期化処理を別関数に分けて、使用時にその関数を呼び出して上げる方法です。

以下がそれを実践したサンプルです。

index.ts
/**
 * 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);

次の解決法ではこの点に焦点を当ててみます。

解決法2:初期化処理を通してコンストラクタを呼び出す

この解決法では以下の対応をします。

  1. コンストラクタ(constructor)privateにして、直接呼び出せないようにする。
  2. インスタンスの初期化処理を非同期の処理プロパティの初期化処理に分けて、それぞれconstructor()init()に記述する。
  3. init()関数をstaticにして、インスタンス生成前に呼び出し可能にする。

この対応を入れると以下のようになります。

index.ts
/**
 * 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()を呼び出すことでしかインスタンス化はできないため、概ね問題なさそうですね!

このエントリーをはてなブックマークに追加


関連記事

記事へのコメント
1:名無しさん
2022年2月19日9:16

React使ってると時々必要になるんだよね〜、コンストラクタに非同期処理入れる方法って

4:名無しさん
2022年2月19日9:39

いいじゃん