TypeScript の assert の使い方

Yuya, web developmenthtmlTypeScript
Back

皆さん TypeScript には assert signature というやつがあるのをご存知でしょうか。

Node.js には assert module というのがあって、例えば以下のコードではコンディションに合ってない場合 AssertionErrorthrow します。

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 っぽい書き方になります!

© Yuya Oiwa