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