TypeScript

anyをやめてunknownと型ガードで安全に書く

読了 6分
TypeScript

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 を実際の型へ絞り込む手段が 型ガード です。基本となるのは typeofinArray.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 を返した分岐の中では、コンパイラが valueUser として扱ってくれます。最初に見たときは「そんな書き方できるんだ」と驚きました。

catchの引数もunknown

try / catchcatch で受け取る値も、既定では 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 前提で扱う

tsconfigstrict を有効にすると、こうした安全な書き方が自然と促されます。「とりあえず any」を「まず unknown」に置き換えるだけでも、見逃されるバグはぐっと減りました。以上です。