Angularによる開発をできるだけ型安全にするためのKabukuでの取り組み
はじめに
こんにちは、フロントエンド担当の速水です。
Kabuku では CostPRO という製造業の加工原価見積もりを行う SaaS を提供しており、 UI の開発には Angular を使用しています。
開発をしていく上で型を検査しデータ不整合を事前に検知したくなることもあると思います。
ただ、Angular で開発する際に、設定次第ではテンプレートで扱う変数の型がany
になることや型が不整合でもエラーが発生しないことがありえます。そのため、実行前のエラー検知が困難になることがあります。
そこで、今回は Angular を使用する際にできるだけ型安全にするために実行したことをまとめようと思います。
実行した内容
テンプレート型チェックの厳格モードを一部有効にしました。それに伴い新たに発生した型エラーに対してngIf
を用いて適切に型を絞り込むように修正を行いました。
オプションを使用することで、テンプレート型チェックの厳密性を設定できます(詳しい説明はこちらに記述されています)。
Angular 9 以降では、 Ivy 有効時に限り、テンプレート型チェックをより厳格化するオプションが利用可能になりました。 Cost PRO 開発は Angular 8 で始めたため、コードベースは新しい型チェックに対応していませんでした。幸い Angular のアップデートには何とかしがみつけており、 Angular 11 で Ivy 有効という状態でした。今回、テンプレート型チェック厳格化オプションを有効化し、エラーに対処しようと試みました。
全てのチェックの有効化は断念しましたが、いくつかのチェックを有効にしたことで、これまで検出できなかった型の不整合をみつけることができました。
有効にするために試みたことと生じた型エラーについて情報を共有したいと思います。
テンプレート型チェックの一部オプションの有効化
テンプレート型チェックを有効にするために、どのようなことをしたのかをまとめていきます。
まず、最初に試みたことはstrictTemplates: true
を設定し厳格な型チェックを有効にすることでした。これは Ivy が有効のときのみ指定可能なオプションであり、テンプレート型チェックについて細かく設定できるオプションとなっています。
ところが、有効にするとエラーが大量に発生してしまい、解消するには時間が必要そうでした。そのため最初の施策としては断念しました。
有効にするのは難しくても段階的にでも型チェックが効くようにしてできるだけ不整合を減らすことができたら嬉しそうです。上記で挙げたページを参照したところ、オプションを個別に指定して有効化できることがわかります。そこで、いくつかのオプションを有効にして型チェックを厳しくすることにしました。
有効にしたのは以下の 6 つのオプションです(各オプションの詳細な説明は型チェックの説明に記載されています)。
- fullTemplateTypeCheck
- strictDomLocalRefTypes
- strictOutputEventTypes
- strictNullInputTypes
- strictAttributeTypes
- strictSafeNavigationTypes
上記のオプションを有効にしたことで、埋め込みビューのチェックの有効、DOM 要素へのローカル参照の型、@Output
と受け取るコンポーネントで型が不一致している箇所、@Input
のnull
チェック、テキスト属性の@Input
のチェック、ナビゲーション操作の戻り値の型チェックを行えるようになりました。これにより今までレビューで見落とされてしまっていた不整合が機械的に検知できるようになり嬉しい状態と言えそうです。
strictTemplatesを有効にした時に起きたエラーの紹介
今回、strictTemplates を有効にすることはできなかったのですが、発生した型エラーの一部は修正を行なっています。その際に気になったエラーについて一部を紹介したいと思います。
共有するエラーの内容としては次の二つです。
- AsyncPipe を利用した箇所で
null
を含んだユニオン型になる - NgSwitch を使用した箇所ではユニオン型が絞り込まれず不整合扱いになる
AsyncPipeを利用した箇所でnullを含んだユニオン型になる
RxJS の Observable をテンプレートにバインディングする際に AsyncPipe を用いていました。ただ、AsyncPipe には初期値がnullになるという問題点を抱えています。これにより、AsyncPipe を用いている箇所では同期的に値が与えられる箇所であっても null
を含んだユニオン型となってしまいます。
この問題に対する解決方法として、
テンプレート型チェックのページでは、 non-null assertions を使用することを一例としてあげています。
ただ、non-null assertions では型を握りつぶしてしまうことになり、のちにnull
やundefined
を含む型となってしまっても型チェックをすり抜けてしまう恐れがあるのでできるだけ使用を避けたいです。
CostPRO での修正としては、AsyncPipe でバインディングした値に対してngIf
を使用し、subscribe した値がnull
でないことを保証するように書き直しました。
NgSwitchを使用した箇所ではユニオン型が絞り込まれず不整合扱いになる
Angular には NgSwitch というディレクティブがあり、以下のように使うことで値に応じて表示するテンプレート切り替えることができます。
<div [ngSwitch]="switch_expression">
<!--switch_expressionが*ngSwitchCaseと同じ箇所のdivを表示 -->
<div *ngSwitchCase="match_expression_1">...</div>
<div *ngSwitchCase="match_expression_2">...</div>
<div *ngSwitchCase="match_expression_3">...</div>
<!--どれにも該当しない場合 -->
<div *ngSwitchDefault>...</div>
</div>
値に応じて変化するテンプレートを記述できるので便利ではあるのですが、NgSwitch では型の絞り込みができず型の不整合が起こり得ます。
TypeScript では switch を使うことで型の絞り込みを行うことができ、たとえば以下の例では型エラーが起きません。
interface A {
type: 'a';
value: string;
}
interface B {
type: 'b';
value: {
prop1: string;
};
}
function typeNarrowingTest(test: A | B) {
switch (test.type) {
case 'a':
console.log(test.value);
break;
case 'b': // Bと判別されているためprop1にアクセスしてもエラーが起きない
console.log(test.value.prop1);
break;
}
}
しかし、NgSwitch では型が絞り込まれないため、以下の記述では型エラーとなります。
// interface A, Bは上の例と同じ型を使用
class NgSwitchTest {
test!: A | B;
}
<div [ngSwitch]="test.type">
<div *ngSwitchCase="’a’">{{test.value}}</div>
<!-- 次のエラーが表示される。
Property 'prop1' does not exist on type
'string | { prop1: string; }' -->
<div *ngSwitchCase="’b’">{{test.value.prop1}}</div>
</div>
NgSwitch で型の絞り込みができないかと調べたところ、型の絞り込みをしたいという要望が出ていたのですが、issue のタグが under consideration でまだ検討中みたいです 。
issue のコメントによると以下のように書くことで NgSwitch を使いながら型を絞り込むやり方はあるみたいです。
<div [ngSwitch]="true">
<!-- isA/Bは各型のユーザー定義のTypeGuard-->
<div *ngSwitchCase="isA(test)">{{test.value}}</div>
<div *ngSwitchCase="isB(test)">{{test.value.prop1}}</div>
</div>
ただ、[ngSwitch]="true"
と true のみを指定しており何の値で絞り込みたいのかがわかりづらくなると感じたこと、そして別途 TypeGuard を用意する必要があり、やや記述量が増えて面倒と感じたため、今回はngIf
を使うように書き直すようにしました。
<div *ngIf="test.type === ’a’">{{test.value}}</div>
<div *ngIf="test.type === ’b’">{{test.value.prop1}}</div>
さいごに
上記のようにテンプレート型エラーを直しオプションを有効にすることでより厳格に型を扱うように試みました。
実行した内容としては NgIf を愚直に使って、抜けていた型の絞り込みをひたすらに行う形となりました。
記事内であげたオプションを有効にすることはできたのですが、strictTemplatesオプションに関してはまだ有効にすることできない状態です。なので、もう少しこの型直しの旅を続けていく必要がありそうです。
このような型の修正は途中から行うとかなり量が増えて大変になると思います。なので、新規開発時は初めから strictTemplatesを有効にするのが良いと思います(Angular12 で新規に開発をする場合はデフォルトで true になっていると思います)。
テンプレートの型チェック以外にもリアクティブフォームを使っている箇所がany
になるなどの型の改良点はまだまだあるので、引き続き型安全にできるように取り組んでいく予定です。
その他の記事
Other Articles
関連職種
Recruit