TypeScriptの型付けでクラス(Class)とインスタンス(Instance)においてハマったポイント

クラスから引っ張ってきたTypeScriptの型付まわりを後回しにしてanyを使いまくっていたら後から型付が面倒になったのですがとりあえずこの記事で知見をまとめたいと思い記事にします。

使用したTypeScriptのバージョンは 4.1.3 でおそらくそれ以下のバージョンにも当てはまる内容かと。

## Firebaseのクラスでのユースケース
実際に自分がハマった型はFirebaseの処理を入れたクラスでした。
以下のコードに対して順を追って自分の場合の型付けの流れを解説。

```firebase.ts
import firebaseConfig from './config';
import firebase from 'firebase';
import { UserSettingsType } from 'models/ProfileTypes';

class Firebase {
  app!: typeof firebase;
  auth!: firebase.auth.Auth;
  db!: firebase.firestore.Firestore;
  functions!: firebase.functions.Functions;
  storage!: firebase.storage.Storage;

  constructor(app: typeof firebase) {
    if (!firebaseInstance) {
      app.initializeApp(firebaseConfig);

      this.app = app;
      this.auth = app.auth();
      this.db = app.firestore();
      this.functions = app.functions();
      this.storage = app.storage();
    }
  }

  emailRegister = () => {
　　　　　　　　// 省略
  };

  login = () => {
　　　　　　　　// 省略
  };

  logout = () => {
　　　　　　　　// 省略
  };

  // 他の処理省略

}

// インスタンス用の変数
let firebaseInstance: InstanceType<typeof Firebase>;

// インスタンス化する関数。引数にはpromise処理した後のfirebaseの結果を代入。
const getFirebaseInstance = (app: typeof firebase): Firebase | null => {
  if (!firebaseInstance && app) {
    firebaseInstance = new Firebase(app);

    return firebaseInstance;
  } else if (firebaseInstance) {
    return firebaseInstance;
  } else {
    return null;
  }
};

export { Firebase };
export default getFirebaseInstance;


```

## 上のクラスとインスタンスに関して自分のハマったポイント
特に意識することなくできている人がいると思いますが自分がハマったポイントを紹介しておきます。

・クラスで宣言した変数に「!」が必要だったところ
・インスタンスに使う型としてInstanceType<hoge>の指定が必要だったところ
・firebaseライブラリの型はPromise処理後のオブジェクトにも適用できたこと（クラスとインスタンスにあまり関係ないけれどfirebaseまわりの型付けはわからなかった）

### クラスで宣言した変数に「!」が必要だったところ

```firebase.ts
  app!: typeof firebase;
  auth!: firebase.auth.Auth;
  db!: firebase.firestore.Firestore;
  functions!: firebase.functions.Functions;
  storage!: firebase.storage.Storage;
```
宣言する際に「!」をつけないと以下のように怒られました。

```
Property 'app' has no initializer and is not definitely assigned in the constructor.ts(2564)
```
このエラーを調べたところ、strictPropertyInitializationのTSオプションによりクラスへの型チェックが厳しくなっていることが原因でした。
クラスで宣言した変数のプロパティは初期化されておらずコンストラクタの中で定義されていないという内容です。

つまり全てのプロパティはコンストラクタの中だけで宣言すれば良いそうみたいでしたが今のままの方が自分的にはわかりやすいコードであるため「!」をつけた方向にしました。

ちなみに「?」でも良いそうですが、「?」をつけると undefined の型候補がついてしまいコードが無駄に増えてしまうと思い「!」にしました。

また tsconfig にて strictPropertyInitialization を false にすれば良いそうですがここ以外でクラスは使っていなかったので firebase.ts のファイルの範囲だけでの許容という形に一応。

### インスタンスに使う型としてInstanceType<hoge>の指定が必要だったところ
インスタンスの型付けには InstanceType という コンストラクタ関数型 が用意されています。
（<a href="https://www.typescriptlang.org/docs/handbook/utility-types.html">公式ドキュメントの InstanceType<Type> 参照 </a>）

それを知らずに typeof Class名 で型付けしましたが別ファイルで呼び出したメソッドで型エラーが生じました。

ちなみに InstanceType<hoge> の hoge の部分には typeof クラス名 を当てはめます。

```firebase.ts
let firebaseInstance: InstanceType<typeof Firebase>;
```



### firebaseライブラリの型はPromise処理後のオブジェクトにも適用できたこと

Class Firebase{以下略} の constructor の引数には Promise処理後 のFirebaseに関わる db, auth, functions などをまとめたオブジェクトを格納しています。
それらのオブジェクトにもそのまま firebase にデフォルトで入っている型を当てはめることができました。

ちなみにこのコードを書くまではライブラリに型がデフォルトで入っていることを知らずググってもわからずかなり詰んでいました。
おまけでそのことも解説しておきます。

#### 型がライブラリにデフォルトで入っていることを知らなかった
import してきた firebase ライブラリの中に型定義ファイルも含まれています。
しかし独学でTSをずっと触ってきたこともあり、これまでライブラリとは別にライブラリに対しても型ライブラリもインストールすることが当たり前でデフォルトで型ファイルも入っていることを知りませんでした。

例えば firebase であれば @firebase/auth などを別途インストールして型を当てはめていくものという認識でした。
実際、npm のサイトにも　@firebase/auth の型ライブラリがありますが非推奨となっています。
「じゃあ代替えの型ライブラリはどこ？」というのがわからず彷徨い続けて any を使うという敗北を味わうこととなりました。

#### firebase/hogeには型が入っていない
もう一つの落とし穴ですが、

```
const auth = import('firebase/auth');
const database = import('firebase/firestore');
const functions = import('firebase/functions');
const storage = import('firebase/storage');
....
```

と言った形で各種ライブラリで型を当てはめようとしたのですが上記のライブラリだと型は入っていませんでした。
(firebase/app には入っています）
中を見ると

```empty-import.d.ts
declare namespace empty {}

export = empty;

```

このようになっています。ここら辺も知っていれば沼にはまらなかったなあという感想でした。