TL;DR

  • null や undefined、空配列で初期化した変数の型は、代入に応じて具体的な型に変化することがある(進化する型)。
  • 進化する型はなるべく使わず、明示的に型アノテーションを付けたほうがよい。

内容

進化する型とは

TypeScript の型は、一般に絞り込まれる方向にしか変わらない。しかし、null や undefined、空配列で初期化した変数の型は、変化することがある。このような型を「進化する型」と呼ぶ。

function range(start: number, limit: number) {
  const nums = []
  //    ^? const nums: any[]
  for (let i = start; i < limit; i++) {
    nums.push(i)
    // ^? const nums: any[]
  }
  return nums
  //     ^? const nums: number[]
}

条件分岐では、分岐ごとに型が変わる。

let value
//  ^? let value: any
if (Math.random() < 0.5) {
  value = /hello/
  value
  // ^? let value: RegExp
} else {
  value = 12
  value
  // ^? let value: number
}
value
// ^? let value: number | RegExp

型が進化するのは、値を代入したり要素を追加したりした後である。進化する型を使うと型アノテーションを減らせる。また、この仕組みを知っておくと、このパターンを使ったコードを読むときに役立つ。

注意点

進化する型が any になるのは書き込むときだけである。読み込むときは暗黙の anyとして型エラーになる。

function range(start: number, limit: number) {
  const nums = []
  // Variable 'nums' implicitly has type 'any[]' in some
  // locations where its type cannot be determined.
  if (start === limit) {
    return nums
    // Variable 'nums' implicitly has an 'any[]' type.
  }
  // ...
}

暗黙の any は、関数呼び出しを通じては進化しない。

function makeSquares(start: number, limit: number) {
  const nums = []
  // Variable 'nums' implicitly has type 'any[]' in some
  // locations where its type cannot be determined.
  range(start, limit).forEach((i) => nums.push(i))
  return nums
  // Variable 'nums' implicitly has an 'any[]' type.
}

明示的に型アノテーションを付けたほうが、意図しないエラーを検出できる。

訳者による解説

本書は進化する型を推奨している。一方、同じ訳者による『初めての TypeScript』では、型チェッカが部分的に働かず、予期しない値が入る可能性があるので、このパターンを非推奨としている。通常の any と同じく好ましくない(項目5)。訳者自身は非推奨の立場である。

キーワード

自分の理解

自分も訳者と同じく、進化する型は使いたくない。any として推論されるということは IDE の補完も効かないはずなので、どのスコープであっても any が発生するのは極力避けたい。