23
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Angularでstrict: trueにした。(strictNullChecks, strictPropertyInitialization編)

Last updated at Posted at 2019-03-15

こんにちは、BBG清水です。

今、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

もともとtsconfig.jsonに含まれていたルール
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": false,

strictNullChecksはfalseになっていました。。
tsのソースコード数が約4万行の環境でstrict: trueにしてみると2700個近いエラーが出ました。
strictBindCallApplyはTypeScript 3.2以降に入ったので、今回対象とはなっていません。

strictNullChecks, strictPropertyInitialization

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

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

TS2564でる
class HogeComponent {
  title: string; // Property 'title' has no initializer and is not definitely assigned in the constructor.
  constructor() {}
}

このエラーがでたプロパティに対しては、次のような対応をする必要があります。

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

reousrce!: T;

2) 宣言をT | null にして、初期状態をnullとする

reousrce: T | null = null;

3) オプショナルプロパティにして、undefinedを許容させる

reousrce?: T;

4) デフォルト値を明示的に設定する

reousrce: T = new T();

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

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

ngOnInitの時には値は絶対あるよね

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

値が「ない」という状態を示したい

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

@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などが対象となりました。

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

初期状態がわかってる

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

  newResource: T = new T();
  @Input() isDisabled: boolean = false;

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

おまけ

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

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を使用するように変更しました

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を考慮していない部分でエラーが発生しました。
例はあまりよくないですが、イメージです↓

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

23
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?