型付き ⊄ 型なし

最近、TypeScriptについて考えることが多い。SideCIでWebフロントエンドの開発に使いはじめたこともあるし、Steepの開発をしていて「TypeScriptだとどうなるんだっけ??」などと言いながら試してみることもある。

TypeScriptは型付きのJavaScriptである。構文はほとんど同じで、使えるライブラリもかなり近い。JavaScriptへの変換はかなり自明で、ランタイムは全く同じ。性能の差はない。Webpackやnpmを初めとするツール群もかなり共通しているし、アプリケーションも似たようなもん。書いている気分には、ほとんど差がない。つまり、TypeScriptとJavaScriptでプログラミングしているときに、なにか違いを感じるとすれば、それは(ほとんど)型付きの言語と型なしの言語の差と考えて良い。

RubyJavaを比較するのとは、全然話が違う。構文も意味も違うし、ランタイムも違うし、使えるライブラリも違う。当然アプリケーションも違う。RubyJavaを比較して、型付き言語と型なし言語の差を感じ取るのは、かなり難しい。


型なしのプログラミング言語に型検査器を入れると、型付きのプログラミング言語になる。逆の方がわかりやすいかな……型付きのプログラミング言語の型検査器を常にacceptするように変更すると、型なしのプログラミング言語ができる。さて、型検査器は、型なしの言語で書かれたプログラムの一部をrejectするものなのだから、型なしのプログラミング言語でプログラムとして受け付けられるものの集合は、型付き言語のそれよりも大きい集合になる。自明だ。

TypeScriptの型検査器をハックして常にacceptするようにすれば、JavaScriptと同じことができる。*1TypeScriptは、JavaScriptプログラムのうちの一部を型エラーが含まれるとしてrejectするものだから、TypeScriptで書けるプログラムの集合は、JavaScriptで書けるプログラムの集合の部分集合になる。TypeScriptプログラムは常にJavaScriptプログラムになるが、JavaScriptプログラムの中にはTypeScriptプログラムでないものがいる。自明だ。

ところがここに「プロダクションにデプロイするプログラム」と注釈をつけると、この部分集合の関係は必ずしも成り立たないことを最近実感している。型検査器を前提にしたときに書けるプログラムの一部は型検査器なしでは怖くて書けないし、型検査器を前提にしたときにできるプログラミングのうちには型検査器なしでは現実的でない行為がある。


代数的データ構造を例として挙げよう。代数的データ構造自体は、型検査器の存在を前提としない。JavaScriptでも書ける。

適当にAjax的なリクエストの結果だと思って見てほしい。

type ErrorResult = {
  id: "ErrorResult",
  code: number
}

type SuccessResult = {
  id: "SuccessResult",
  body: any
}

type HttpResult = ErrorResult | SuccessResult

const errorResult = { id: "ErrorResult", code: 404 }
const successResult = { id: "SuccessResult", body: [1,2,3] }

function processResult(result: HttpResult): string {
  switch (result.id) {
    case "ErrorResult":
      return "Error"
    case "SuccessResult":
      return "Success"
  }
}

別にJavaScriptでも書ける。

const errorResult = { id: "ErrorResult", code: 404 }
const successResult = { id: "SuccessResult", body: [1,2,3] }

function processResult(result) {
  switch (result.id) {
    case "ErrorResult":
      return "Error"
    case "SuccessResult":
      return "Success"
  }
}

確かに書けるんだけど、まあJavaScriptでこんなコードを書くのは、結構肝が据わっていないとできないことだと思う。どう考えても問題があるコードで、

  • codebody を間違えたら困る
  • 将来的に新しい結果の種類を増やしたいときに、全ての switch を忘れずに更新できるかどうかの賭けはしたくない

などがすぐに思いつくはずだ。

一方、このプログラムはTypeScriptでは問題がない。

  • code とか body とかを間違えても型検査が見つけてくれる
  • 新しい型を増やしたときにも switch の分岐が網羅的か、型検査器がチェックしてくれる

プロダクションに自信を持って送り込めるコードになった。つまり、この前提では、型検査器によって書けるコードが増えている。


こんなことを考えるようになったのは、やっぱりTypeScriptを書いているからだと思う。型検査の有無しか違わない二つの言語で、プログラミングの心構えが変わるのは、型検査の有無が原因と考えるのが妥当だろう。Objective Cを書いていたときには、コンパイラのバージョンが上がって機能が増えたことがけっこうあったが、「機能が増えたから書けるプログラムが増えるのは当然」としか思わなかった。大体の新機能は制約を緩めるために導入されるものなので、書けるプログラムが増えるのはやっぱり当然である。

「ある種の検査器を導入すると、原理的には書けるプログラムの集合は小さくなるはずなのに、逆にこれまでは(現実的に)書けなかった種類のプログラムが書けるようになる」という体験は、面白いと思う。

*1:全部any型を付ければ大体なる。