TypeScript の assert の使い方
皆さん TypeScript には assert signature というやつがあるのをご存知でしょうか。
Node.js には assert module というのがあって、例えば以下のコードではコンディションに合ってない場合 AssertionError
を throw
します。
assert(0 > 2)
普段はこのような使い方されてますね。
function add(a, b) {
assert(typeof a === 'number');
assert(typeof b === 'number');
return a + b;
}
ですが TypeScript では上のような書き方でも型を推測することはできず、ちゃんと型情報を提供してくれません。
ここで使うのが assert signature です。
シンタックスはこんな感じ。
function assertIsNumber(a: any) asserts a is number {
if (typeof a !== number) {
throw Error('Not a number');
}
}
function add(a, b) {
assertIsNumber(a);
assertIsNumber(b);
return a + b;
}
リアルなシチュエーションで言うと、例えばフォームとかでPOSTリクエストを送ってサーバー側でパラメータを使う場合。
// フロント
<form method="post">
<input type="text" name="username" placeholder="John Doe" />
<input type="password" name="password" />
<button type="submit">Submit</button>
</form>
// サーバー側で
const body = new URLSearchParams(await request.text());
const payload = {
username: body.get("username"), // string | null
password: body.get("password"), // string | null
};
body.get()
のリターン値は string | null
となっているので、上の payload
の情報を使ってユーザーを作るとしても、もしその API にちゃんと型情報がついている場合はまず null
ではないことを確認しないといけません。
const payload = {
username: body.get("username"), // string | null
password: body.get("password"), // string | null
};
try {
// createUser の型は username: string, password: string なのでTSに怒られますね
await createUser(payload.username, payload.password);
...
} catch(e) {
...
}
ここで多分ほとんどの人がするのが type casting
だと思います。
const payload = {
username: body.get("username") as string,
password: body.get("password") as string,
};
これで一応解決はできるのですが、なんかあんまりTSの恩恵を受けれてないですよね。マニュアルですし。
ここで assert signature を使いましょう。
type NonNullProperties<Type> = {
[Key in keyof Type]: Exclude<Type[Key], null>;
};
function assertNonNull<T extends Record<string, null | unknown>>(
obj: T
): asserts obj is NonNullProperties<T> {
for (const [key, val] of Object.entries(obj)) {
if (val === null) {
throw new Error(`The value of ${key} is null but it should not be.`);
}
}
}
上のヘルパーアサーションメソッドはオブジェクトを受け取り、 Object.entries(obj)
をループして null
が存在すれば throw
するやつです。
では先程の例に使ってみましょう。
const payload = {
username: body.get("username"), // string | null
password: body.get("password"), // string | null
};
assertNonNull(payload);
/*
ここまで来ると throw されていないと言うことなので payload の型は null が抜かれた型だとTSは認識してくれる
payload = {
username, // string
password // string
}
*/
try {
// エラーが起きない
await createUser(payload.username, payload.password);
...
} catch(e) {
...
}
どうでしょうか。Type cast した方が確かに楽になるのですが、assertion helper を一度作ってしまうと汎用性が効く・TSを騙してない(?)のでTSっぽいといえばTSっぽい書き方になります!
Discussion