AngularのInjectableまとめ


### 経緯

https://www.toptal.com/angular/state-management-in-angular-using-firebase

こちらのページで学習中、`NG0200: Circular dependency in DI detected for EmployeesService. `のエラーに遭遇しました。

```typescript:firestore.service.ts
// @Injectableを書かないと、このservice内でDIしているAngularFirestoreにアクセスした際に
// NG0200: Circular dependency in DI detected for EmployeesService. が出る
@Injectable()
export abstract class FirestoreService<T> {

}
```

https://stackoverflow.com/questions/60283863/getting-error-in-generic-firestore-service-while-extending-it-after-upgrading-t

エラーの解決方法としては上記の通り`@Injectable()`を付けるだけなのですが、せっかくなので内部で何が起きているかを調べてみたいと思います。

---

### ng-japan OnAir vol.30 "Injectableを再発見！"

まずはこちらで学習します。

https://www.youtube.com/watch?v=kf9sPXeuAok

（参考）
[公式サイト：Angular-Injectable](https://angular.io/api/core/Injectable)
[Angular v6で導入されるTree-Shakable DIの紹介](https://blog.lacolaco.net/2018/03/angular-v6-tree-shakabke-di/)
[Angular2のDIを知る](https://qiita.com/lacolaco/items/61eed550d1f6070b36ab)

#### Injectableについて

- `Injectable`は、そのclassを提供（provide）可能・注入（inject）可能にするデコレータ
- 逆に言えば、その`class`が注入されるためには、`Injectable`がついていなければいけない（`constructor`でDIされる`class`には基本的に`Injectable`が必要）
- v2から登場した`Injectable`に、v6で`providedIn`のオプションが入った
- Tree-Shakable
- v5までは、DIする場合は`NgModule`の`provider`に登録しなければならなかった（`NgModule`から`class`を参照する）
- どこからも`Injection`されない`class`でも、`Provider`に登録する時点で`NgModule`からの参照が発生し、バンドルに入ってしまうという問題があった
- そこでv6で`@Injectable`に機能追加を行い、どの単位で参照するかを`class`自身が持つようにした（`providedIn`で指定）
- これにより逆に`class`が`NgModule`を参照するようになり、自身が`Component`や他の`class`から参照されなければ、どこからも参照されない状態になった

>@Injectable()のprovidedInプロパティを使用することは、 @NgModule()のproviders 配列を使用するよりも望ましい方法です。 なぜなら、@Injectable()のprovidedInを使用することで、 最適化ツールはアプリケーションで使用されていないサービスをツリーシェイキングし、 バンドルサイズをより小さくできるからです。
（[公式サイト](https://angular.jp/guide/hierarchical-dependency-injection)より）

- `providedIn`のoptionとしては`root`、`platform`、`any`があるが、ほとんど`root`しか使わない
- 'root' : The application-level injector in most apps.
- angularアプリに対して1個のインスタンスを作る（bootstrap単位でのシングルトン）

#### ngcコマンドの結果を使用したデバッグについて

- `ngc` コマンドでコンパイル後のjsを見ることができる
- `@Injectable`は`ɵfac`と`ɵprov`を作る
- `ɵfac`はその`class`を`Inject`するためのインスタンスをどうやって作るかを定義するFactory関数
- `ɵfac`を通ってインスタンスが作られないとDIされない
- つまり「`Inject`されるためには`Injectable`がついていなければいけない」というのは、`ɵfac`を作ってインスタンスの作り方を定義しなければならないということ
- `ɵprov`は`Injectable`をどうprovideするかの設定情報
- `hogeClass.ɵfac is not function`のエラーは`Injectable`の付け忘れ
- `ɵ`はキーボードで入力できないことから、参照しないでアピールで使う
- Angularに詳しくなりたい方は`ngc`の結果を読むのはオススメ

---

### コンパイル後のjsを比較してみる

次の3パターンでngcコマンドでコンパイルされたjsを比較します。

| パターン | `@Injectable` | `provideIn` |
|:-:|:-:|:-:|
| 1 | あり | なし |
| 2 | あり | root |
| 3 | なし | - |

#### パターン1 : `@Injectable`あり、`provideIn`指定なし
```firestore.service.ts
import { AngularFirestore, QueryFn } from "@angular/fire/compat/firestore";

@Injectable()
export abstract class FirestoreService<T> {

  constructor(protected firestore: AngularFirestore;) {}

}
```

```firestore.service.js
import * as i0 from "@angular/core";
import * as i1 from "@angular/fire/compat/firestore";
export class FirestoreService {

}
FirestoreService.ɵfac = function FirestoreService_Factory(t) { return new (t || FirestoreService)(i0.ɵɵinject(i1.AngularFirestore)); };
FirestoreService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: FirestoreService, factory: FirestoreService.ɵfac });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(FirestoreService, [{
        type: Injectable
    }], function () { return [{ type: i1.AngularFirestore }]; }, null); })();
```
`ɵfac`と`ɵprov`が作られていることが分かります。

#### パターン2 : `@Injectable`あり、`provideIn:'root'`

```firestore.service.ts
import { AngularFirestore, QueryFn } from "@angular/fire/compat/firestore";

@Injectable({
  providedIn: 'root'
})
export abstract class FirestoreService<T> {

  constructor(protected firestore: AngularFirestore;) {}

}
```

```firestore.service.js
import * as i0 from "@angular/core";
import * as i1 from "@angular/fire/compat/firestore";

FirestoreService.ɵfac = function FirestoreService_Factory(t) { return new (t || FirestoreService)(i0.ɵɵinject(i1.AngularFirestore)); };
FirestoreService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: FirestoreService, factory: FirestoreService.ɵfac, providedIn: 'root' });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(FirestoreService, [{
        type: Injectable,
        args: [{
                providedIn: 'root'
            }]
    }], function () { return [{ type: i1.AngularFirestore }]; }, null); })();
```

[ɵɵdefineInjectable](https://github.com/angular/angular/blob/cd0096df2cb182fc7ec370f2ff7c6d99f4639215/packages/core/src/di/interface/defs.ts) の引数となっているオブジェクトのプロパティに`providedIn: 'root'`が追加されています。

#### パターン3 : `@Injectable`なし

```firestore.service.ts
import { AngularFirestore, QueryFn } from "@angular/fire/compat/firestore";

// @Injectable()   <-- なし
export abstract class FirestoreService<T> {

  constructor(protected firestore: AngularFirestore;) {}

}
```

```firestore.service.js
// import * as i0 from "@angular/core";  <-- なくなった
// import * as i1 from "@angular/fire/compat/firestore";  <-- なくなった
export class FirestoreService {

}
// ↓ なくなった
// FirestoreService.ɵfac = function FirestoreService_Factory(t) { return new (t || FirestoreService)(i0.ɵɵinject(i1.AngularFirestore)); };
// FirestoreService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: FirestoreService, factory: FirestoreService.ɵfac });
// (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(FirestoreService, [{
//         type: Injectable
//     }], function () { return [{ type: i1.AngularFirestore }]; }, null); })();
```

`ɵfac`と`ɵprov`が作られていません。
また、factory関数内の`i0.ɵɵinject(i1.AngularFirestore)`が消え、そもそも`AngularFirestore`がimportされていません。

`firestore.service.ts`に`@Injectable()`をつけないと`NG0200: Circular dependency in DI detected for EmployeesService.` が出る問題は、これが原因と思われます。

が、なんで`Circular dependency`になるのかは分かりません・・・

---

- `/*@__PURE__*/`について

https://www.kabuku.co.jp/developers/tree-shaking-in-2018



---

### `@Injectable()`（`ProvidedIn`の指定なし）は何を意味するのか？

> **providedIn**
Determines which injectors will provide the injectable.
>
> providedIn?: Type<any> | 'root' | 'platform' | 'any' | null
> - 'null' : Equivalent to undefined. The injectable is not provided in any scope automatically and must be added to a providers array of an @NgModule, @Component or @Directive.
>
>（[公式サイト](https://angular.io/api/core/Injectable)より）


- `providedIn`は、どの`injector`が`injectable`をprovideするか決めるもの
- `@Injectable()` と `@Injectable({providedIn:null})`同じで、どのスコープにも自動的にはprovideされない

ちなみに、`injector`は3種類あります
| Injector | configuredBy | providedIn option |備考|
|:-:|:-:|:-:|:-:|
| NullInjector |   |   |`@Optional`を使わない限りエラーを投げる|
| ModuleInjector | PlatformModule | platform ||
| root ModuleInjector | AppModule | root ||


https://angular.jp/guide/hierarchical-dependency-injection

```firestore.service.ts
@Injectable()
export abstract class FirestoreService<T> {

}
```

`@Injectable()`をつけただけではprovideされないので、`FirestoreService`をどこかの`class`で`Inject`するとエラーが出ると予想されます。

```employees.service.ts
import { FirestoreService } from './firestore.service';

export class EmployeesService {
  constructor(private fs: FirestoreService<Employee>) { }
}
```
実行するとやはりエラーが出ました。
`NullInjectorError: No provider for FirestoreService!`

他の`class`で使いたい`class`には`@Injectable()`を付けるだけでは十分でなく、`providedIn`で`Injector`を決めてやる必要があるようです。