assertsでassert関数なんて記事を以前に書いたんですが、assert関数より例外を投げるだけの関数を用意した方が汎用性が高そうな気がしてきました。
/**
* テンプレートリテラルで生成した文字列をメッセージとした例外を投げる。
* @param templ 例外のメッセージに指定する文字列のテンプレート
* @param values テンプレートに適用する値
*/
function error(templ: TemplateStringsArray, ...values: unknown[]): never;
/**
* メッセージ無しの例外を投げる。
*/
function error(): never;
/**
* 指定した文字列をメッセージとした例外を投げる。
* @param message 例外のメッセージに指定する文字列。
*/
function error(message: string): never;
function error(): never {
const message =
// 引数無しの場合はメッセージ無し
arguments.length === 0
? undefined
: // 引数が文字列の場合はそのままメッセージにする
typeof arguments[0] === 'string'
? arguments[0]
: // タグ付きテンプレートリテラルの場合はテンプレートからメッセージ生成
(arguments[0] as string[]).reduce(
(r, t, i) => r + String(arguments[i]) + t
);
throw new Error(message);
}
という関数を用意しておくと、
function compare<T>(a: T, b: T): 0 | 1 | -1 {
// 等しいときは0
return a === b ? 0 :
// 前者の方が小さい場合は-1
a < b ? -1 :
// 前者の方が大きい場合は1
a > b ? 1 :
// どれでも無い場合は例外
error`Uncomparable value: ${a}, ${b}`;
}
のように想定外の値が来た場合は例外を投げる、ということが簡単に書けます。
例えばある変数をnullチェックしたあと、その変数をハンドラの中で使おうとしたとき、nullの可能性があると指摘されたことはないですか?
canvas.addEventListener('mousedown', ev => {
let ctx = canvas.getContext('2d');
if (!ctx) {
return;
}
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
const {x, y} = eventToPos(ev);
ctx.moveTo(x, y);
canvas.addEventListener('mousemove', ev => {
const {x, y} = eventToPos(ev);
ctx.lineTo(x, y); // ctxはnullの可能性がある(nullチェックしたのに...)
})
}
こんなときでも
let ctx = canvas.getContext('2d') ?? error`コンテキストの取得失敗`;
と書いておけば、ctxは最初からnullにはならないため、ハンドラの中で使ってもエラーになりません。
assertsでassert関数で挙げた例でいえば、
function index(node: INode): number {
return node.parent
? (
node.parent.children ?? error`nodeのparentにはchildrenがいるはず(少なくともnodeが)`
).indexOf(node)
: 0;
}
のように書くことでassert関数いらずに。
たまに!
を使ってnullチェックをはしょっているコードを見かけますが、それよりチェックして例外投げるようにした方が何が原因かを例外のメッセージに書くことができます。
function func(arg?: {aaa?: {bbb?: {ccc?: string}}}) {
console.log(arg!.aaa!.bbb!.ccc!.slice()); // もしどこかがundefinedだったときにはエラーになる
console.log(arg?.aaa?.bbb?.ccc?.slice() ?? error`必須パラメータがないよ`); // どうせエラーになるならエラーの情報を載せよう
}
注意点
何故かタグ付きテンプレートリテラルだけの文だと返値のnever型が有効にならず、その後の式で型ガードが有効になりません。
function func(arg: {type: 'string' | 'number', value: string | number}) {
if (arg.type === 'string') {
if (typeof arg.value !== 'string') {
error`typeがstringなのにvalueが文字列じゃない`;
}
arg.value; // なぜかここでもarg.valueはstring | number
}
// ...
}
こんなときはタグ付きテンプレートリテラルではなく、文字列を引数に取るタイプを使用して下さい。
function func(arg: {type: 'string' | 'number', value: string | number}) {
if (arg.type === 'string') {
if (typeof arg.value !== 'string') {
error(`typeがstringなのにvalueが文字列じゃない`);
}
arg.value; // これならarg.valueはstring
}
// ...
}