🤏

TypeScript の assert の使い方

2021/06/27に公開

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

Discussion