これはAngularアプリケーションの開発においても表明 (Assertion)を取り入れてみようという実験である。簡単なサンプルはGitHubで公開しているので、興味があればそちらも見てもらえるといい。
Assertion in Browsers
Node.jsと違い、ブラウザ環境にはランタイム標準の assert
関数はないので、自前でどうにかする必要がある。その実装詳細はここではどうでもよいので素朴に実装する。関数は2種類あり、ひとつは unsafeAssert
で、表明した事前条件が満たされなければ例外を投げる。もうひとつの safeAssert
はコンソールログでアサーションエラーが表示されるのみで例外は投げない。
/**
* This function is an assert function.
* If the condition is false, it throws an error with the given message.
*/
function unsafeAssert(condition: boolean, message: string) {
if (!condition) {
const error = new Error(message);
error.name = 'AssertionError';
throw error;
}
}
/**
* This function is a safe version of the assert function.
* It never throws an error, but instead logs the error message to the console.
*/
function safeAssert(condition: boolean, message: string) {
console.assert(condition, message);
}
Toggle Assertion Strategy
上記の2つの関数を、実行モードにより切り替えたい。具体的には、ローカルでの開発時やステージング環境などでは例外を投げる unsafeAssert
を使いたいが、プロダクション環境では safeAssert
にしたい。
この戦略のトグルを実装するために、Angular v17.2で導入されたAngular CLIの define
機能を使ってみよう。 THROW_ASSERTION_ERROR
というグローバル変数が true
であるときにunsafeAssert
を使うようにするセットアップ関数を実装する。
/**
* This function sets up the global `assert` function.
* If `THROW_ASSERTION_ERROR` is true, it sets the global `assert` function to `unsafeAssert`, which throws an error when the condition is false.
* Otherwise, it sets the global `assert` function to `safeAssert`, which logs the error message to the console.
*/
export function setupGlobalAssert() {
if (THROW_ASSERTION_ERROR) {
window.assert = unsafeAssert;
} else {
window.assert = safeAssert;
}
}
このコードが型チェックを通過できるように、 global.d.ts
のようなファイルで型定義をしておくのも必要だ。
// src/global.d.ts
declare const THROW_ASSERTION_ERROR: boolean;
declare var assert: (condition: boolean, message: string) => void;
そしてsetupGlobalAssert
をmain.ts
で呼び出せば準備完了だ。
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
import { setupGlobalAssert } from './lib/assert';
setupGlobalAssert();
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
);
あとは、angular.json
を開いてビルドオプションのデフォルト設定と development
のときの切り替えをそれぞれ行うといい。
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
...
"define": {
"THROW_ASSERTION_ERROR": "false"
}
},
"configurations": {
"production": {...},
"development": {
...
"define": {
"THROW_ASSERTION_ERROR": "true"
}
}
},
"defaultConfiguration": "production"
},
こうすることで、コード中のTHROW_ASSERTION_ERROR
はビルド時にfalse
かtrue
に置換される。条件分岐のどちらを通るかがビルドのタイミングで決定できるため、Angular CLIはTree Shakingによって使わない方のassert関数をデッドコードとして削除できる。
Assertion in Production
このように準備を整えると、次のように(この例の条件は適当だが)コンポーネントで気軽に表明ができる。もちろんコンポーネントじゃなくてもアプリケーションのどこででもできる。
import { Component } from '@angular/core';
@Component({
...
})
export class AppComponent {
title = 'ng-assertion';
ngOnInit() {
assert(this.title === 'ng-assertion', 'Title is not ng-assertion');
}
}
assert関数が十分に軽量であれば、テストコードではなくアプリケーションコードの側にこのような表明を書いていくことで、ユーザーへの悪影響を最小限にして契約による設計を取り入れていけるのではなかろうか。