Angularでstrict: trueにした。（strictNullChecks, strictPropertyInitialization編）

こんにちは、[BBG](https://bbank.jp/)の[清水](https://twitter.com/shimikyonkyon)です。

今、tsconfig.jsonのcompilerOptionsで`strict: true`にする作業をしています。
（「Angularでstrict: trueにした。」と言っていますが、絶賛作業中です。）

### strict: true
compilerOptions内で`strict: true`とすると以下のルールが有効になります。
- --noImplicitAny
- --noImplicitThis
- --alwaysStrict
- --strictBindCallApply
- --strictNullChecks
- --strictFunctionTypes
- --strictPropertyInitialization

今回は、`strict: true`にしている過程でとても悩んだ、strictNullChecks, strictPropertyInitializationのことについて話します。


### 作業していた環境
Angular: 6.0
typescript: 2.7

```json:もともとtsconfig.jsonに含まれていたルール
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": false,
```
strictNullChecksはfalseになっていました。。
tsのソースコード数が約4万行の環境で`strict: true`にしてみると2700個近いエラーが出ました。
[strictBindCallApplyはTypeScript 3.2以降](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-2.html)に入ったので、今回対象とはなっていません。

### strictNullChecks, strictPropertyInitialization
strictNullChecks　　　　　： nullとundefinedは、それ自身またはany以外の型に許容させない
strictPropertyInitialization　： undefinedを許容していないプロパティを初期化させる

もともとこれらのルールを考慮していなかったので、undefinedが許容されていないプロパティの宣言部分でTS2564エラーになる箇所が大量発生しました。


```typescript:TS2564でる
class HogeComponent {
  title: string; // Property 'title' has no initializer and is not definitely assigned in the constructor.
  constructor() {}
}
```
このエラーがでたプロパティに対しては、次のような対応をする必要があります。

**１）　Non-nullアサーションオペレータを使用して、プロパティがNon-Nullableであることを明示的に表す**

```typescript
reousrce!: T;
```

**２）　宣言をT | null にして、初期状態をnullとする**

```typescript
reousrce: T | null = null;
```
**３）　オプショナルプロパティにして、undefinedを許容させる**

```typescript
reousrce?: T;
```
**４）　デフォルト値を明示的に設定する**

```typescript
reousrce: T = new T();
```

これらの使い分けは、個々のプロパティがどうあるべきかを考えながら検討する必要があります。
また、対応方針が全体的にブレないようにすることも重要です。

私は次のような方針のもと、１） ~ ４）の対応を使い分けました。

#### ngOnInitの時には値は絶対あるよね
ngOnInitが呼び出される際に必ず値が入っていることが前提とされているものは、**Non-nullアサーションオペレータ**をつける対応をしました。
必ず@Inputで受け取ることがわかっているプロパティなどが対象となりました。

#### 値が「ない」という状態を示したい
対象プロパティの値がある・なしで表示状態や挙動を変えたいようなケースの場合、**プロパティの型をT | null にして、初期状態をnull**としました。
主にapiから取得したデータを表すプロパティが対象となりました。

```typescript
@Component({
    ...
  template: `
    <ng-content *ngIf="resource; else noResource">
      "I have a resource."
    </ng-content>
    <ng-template #noResource>"No resource."</ng-template>
  `,
})
class HogeComponent implements OnInit {
  resource: T | null = null;
  url = 'url';

  constructor(private http: HttpClient) {}
  ngOnInit() {
    this.http.get<T>(this.url)
      .subscribe((res) => this.resource = res));
  }
}
```

#### 値はあってもなくてもいい

対象プロパティがあってもなくてもいいものは**オプショナルプロパティにして、undefinedを許容**させました。
状態に依存するstring型のプロパティや、@ViewChild, @ContentChildなどが対象となりました。

```typescript
  labelKey?: string;
  @ViewChild('noResource') noResource?: ElementRef;
```


#### 初期状態がわかってる
初期状態がわかるものは**デフォルト値を明示的に設定**しました。
新規作成データ用のプロパティや、画面制御系のboolean型プロパティが対象となりました。

```typescript
  newResource: T = new T();
  @Input() isDisabled: boolean = false;
```
画面制御用のboolean値は前行程の処理によって変わるものが多いですが、通常状態を初期状態と認識して初期値を設定させました。
オプショナルにするかも悩みましたが、boolean値がundefinedである状態を作りたくなかったのもありやめました。


#### おまけ

unsubscribeをする用に、Subscription型のプロパティを用意している箇所があったのですが、

```typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

@Component({...})
class HogeComponent implements OnInit, OnDestroy {
  subscription: Subscription; // こんなの
  selectedId$ = new BehaviorSubject<string | null>(null);

  constructor() {}

  ngOnInit() {
    this.subscription = this.selectedId$.subscribe((id) => { ... });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}
```

rxjsのtakeUntilを使用するように変更しました

```typescript
import { Component, OnInit, OnDestroy, EventEmitter } from '@angular/core';
import { takeUntil } from 'rxjs/operators';

@Component({...})
class HogeComponent implements OnInit, OnDestroy {
  selectedId$ = new BehaviorSubject<string | null>(null);
  private readonly onDestroy$ = new EventEmitter();

  constructor() {}

  ngOnInit() {
    this.selectedId$
      .pipe(takeUntil(this.onDestroy$))
      .subscribe((id) => { ... });
  }

  ngOnDestroy() {
    this.onDestroy$.emit();
  }
}
```



### いったんまとめ
「プロパティごとにどうあるべきか」かつ、「方針が現実的か」を考えるのが大変でした。
ある時を境に、**「初期値とは。」**という疑問に対してゲシュタルト崩壊を起こしました。
ただし、全体を見ながら修正していくことにより危険なソースの抽出や、今まで何も考えずに宣言されていたプロパティの見直しができました。


### まだまだ終わらぬ。
TS2564を消化してく過程でプロパティをオプショナルやnullableにしたことにより、今度はそれらを使用する側でオプショナルやnullableを考慮していない部分でエラーが発生しました。
例はあまりよくないですが、イメージです↓

```typescript:TS2322でる。
class HogeComponent {
  somethingId: string | null = null;

  constructor(private searchService: SearchService,) {}

  somethingOccurred() {
    this.searchService.get<Hoge>(this.somethingId) // 'string | null' is not assignable to type 'string'.
      .subscribe((res) => ...));
  }
}

class SearchService {
  url = 'url';

  constructor(private http: HttpClient) {}

  get<T>(id: string): Observable<T> {
    return this.http.get<T>(`${this.url}/${id}`);
  }
}
  
```


`strict: true`対応が全部完了するまであと少しなので、完了したら後編ブログを書こうと思います。

### 参考にした記事
https://lacolaco.hatenablog.com/entry/2018/04/10/230413
https://lacolaco.hatenablog.com/entry/2018/06/27/125101