anyをやめてunknownと型ガードで安全に書く
anyをやめてunknownと型ガードで安全に書く
型が決まらない値に出会うと、つい any を置いて先に進めたくなります。自分もよくやっていたのですが、それで実行時エラーを取りこぼしたので、unknown に置き換えたときのメモです。
any は「型チェックを止める」宣言で、そこから先のバグをコンパイラが見逃すようになります。多くの場合、本当に欲しいのは unknown のようです。違いを理解して、安全側に倒しておくとよさそうです。
anyは型チェックを無効化する
any 型の値は、どんな操作をしてもコンパイラが文句を言いません。一見便利ですが、これは「間違った操作も素通りする」ということです。
function handle(value: any) {
value.toUpperCase() // 数値でもエラーにならない
value.foo.bar // 存在しなくても通ってしまう
const n: number = value // 何にでも代入できてしまう
}
実行時には value が数値だった瞬間に例外になりますが、コンパイル時には何も警告されません。any が1つ混ざると、そこを経由した値の型情報がごっそり失われて、安全性が連鎖的に崩れていきます。これが地味に怖いところです。
unknownは「使う前に確かめろ」と促す
unknown も「何でも入る」型ですが、any と決定的に違うのは そのままでは操作できない 点です。何かをする前に、型を絞り込む(ナローイングする)ことを強制されます。
function handle(value: unknown) {
value.toUpperCase() // エラー:unknown のままでは呼べない
if (typeof value === 'string') {
value.toUpperCase() // OK:string だと確定したので呼べる
}
}
「入口は広く受けるけど、使うときには必ず確認する」という安全な流れを、型の力で守れるわけです。外部から来る信頼できない値(APIレスポンス、JSON.parse の結果、catch の引数など)はまさに unknown が適任だと思います。
型ガードで絞り込む
unknown を実際の型へ絞り込む手段が 型ガード です。基本となるのは typeof と in、Array.isArray などの組み込みチェックです。
function getLength(value: unknown): number {
if (typeof value === 'string') return value.length
if (Array.isArray(value)) return value.length
return 0
}
オブジェクトのプロパティを確認するときは in 演算子が使えます。
function getName(value: unknown): string {
if (typeof value === 'object' && value !== null && 'name' in value) {
// ここで value は { name: unknown } を含むと分かる
return String((value as { name: unknown }).name)
}
return '名無し'
}
ユーザー定義型ガードで意図を表す
複雑な形のチェックは、is を使った ユーザー定義型ガード に切り出すと再利用できて、呼び出し側のコードもすっきりします。
type User = { id: string; name: string }
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
typeof (value as Record<string, unknown>).id === 'string' &&
typeof (value as Record<string, unknown>).name === 'string'
)
}
async function loadUser(): Promise<User | null> {
const data: unknown = await fetch('/api/me').then((r) => r.json())
return isUser(data) ? data : null // チェックを通れば User として扱える
}
value is User という戻り値の書き方が肝で、これが true を返した分岐の中では、コンパイラが value を User として扱ってくれます。最初に見たときは「そんな書き方できるんだ」と驚きました。
catchの引数もunknown
try / catch の catch で受け取る値も、既定では unknown です(useUnknownInCatchVariables が有効な場合)。何でも throw できる以上、エラーオブジェクトだと決めつけず、確認してから使うのが安全です。
try {
await doSomething()
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '不明なエラー'
console.error(message)
}
err.message をいきなり触りに行ってエラーになったことがあるので、ここは instanceof で確認してから使うようにしています。
まとめ
anyは型チェックを止めてしまう。安易に使うと安全性が連鎖的に失われる- 「何でも入る」値が欲しいときは、まず
unknownを検討する unknownは型ガード(typeof/in/is)で絞り込んでから使う- 外部入力や
catchの値はunknown前提で扱う
tsconfig で strict を有効にすると、こうした安全な書き方が自然と促されます。「とりあえず any」を「まず unknown」に置き換えるだけでも、見逃されるバグはぐっと減りました。以上です。