合法 TypeScript 第2章 TypeScript 山脈を見下ろす
Tweetこれからの何章かは、TypeScript という言語そのものの紹介をしていきます。TypeScript Compiler(TSC) がどのように機能するのか、それからTypeScript の各機能の紹介、さらにこれらの機能を使ってどのような開発をすることができるのかといったことです。まずはコンパイラーから始めることにしましょう。
The Compiler
これまでどんな言語を使ってきたかによって、プログラムがどのように機能するのかということについてあなたが持っている概念は異なるはずです。TypeScript は、JavaScript や Java といった一般的で主流な言語とはそれが異なるので、まずこれを理解していきましょう。
ざっくりいうと、プログラムはプログラマーが書いた大量のテキストを保持するファイルです。テキストは、compiler とよばれるプログラムによって parse = 品詞分解され、abstract syntax tree(AST)に変換されます。AST とは空白とかコメントとか、それからあなたがタブ派なのかスペース派なのかといったどうでもいい闘争に関する情報を全て取っ払った「構造化された情報」です。さらに compiler によって AST は「より低次元な表現」である「bytecode」に変換されます。この bytecode を compiler とは別のプログロムである runtime に与えると、runtime は bytecode を評価し、そしてあなたはその結果を得ることができます。ですからあなたがプログラムを実行するというのは、あなたのソースコードを compiler にパースさせて AST へと変換させ、さらに bytecode へと変換させ、そうしてできた bytecode を runtime に評価させることなのです。その詳細は言語によって多少異なりますが、大抵の言語については抽象化されたレベルではこの説明で正確といっていいでしょう。
もう一度ステップごとに整理しましょう。
- プログラムはパースされ、AST に変換される
- AST は bytecode にコンパイルされる
- Bytecode がランタイムによって評価される
TypeScript が他の言語と異なる特殊な点は、bytecode に直接コンパイルされるのではなく、JavaScript のコードにコンパイルされる点なのです!TypeScript からコンパイルされ生成された JavaScript のコードは、いつも の JavaScript のコードを実行しているのと同じように実行することができます。つまり、ブラウザに実行させたり、NodeJS に実行させたり、もしくは紙と鉛筆を持って手書きで実行することもできます(これは機械が人間に反乱を起こした後の世紀に、この本を手に取った人たちのためのおすすめの手法です)
この段階になってあなたは気づいたかもしれません。「ちょっと待って!前章で TypeScript はコードを安全にしてくれると言いましたよね?でもそれはどの段階で起きるっていうのよ!?」
いい質問ですね(feat. 池上彰)私は重要なステップについての説明を抜かしてしまいましました。それは TypeScript Compiler が AST を生成した後、しかし JavaScript のコードを生成する前のステップです。このタイミングで TypeScript はあなたの書いたコードの型チェックを行うのです。
Typechecker: あなたのコードが typesafe であるかどうかを点検するためのプログラムのこと。
そうです。これこそが TypeScript が見えないところで行使している魔術なのです。Typechecker が typecheck をすることで、プログラムが意図した通りに動くのか、明らかな誤りがないのかを確認することができ、そして5兆円のオファーがくることにもなるのです。(焦らないで。まだその電話がきていないとしても、単に彼らの手が空いていないだけなのです。気長に待ちましょう。)
Typecheck と JavaScript コードの生成も含めると、これまで説明してきたステップは以下のように整理できます。
TS が実行すること
- TypeScript source => TypeScript AST
- AST が typechecker でチェックされる
- TypeScript AST => JavaScript source
JS が実行すること
- JavaScript source => JavaScript AST
- AST => bytecode
- Bytecode が runtime によって評価される
step 1-3 は TSC によって行われ、step 4-6 は JavaScript runtime によって例えばブラウザや NodeJS といったあなたが使っている JavaScript engine によって処理されます。
ちょっとしたメモ:JavaScript の compiler と runtime は大抵の場合 engine と呼ばれるプログラムの中に全部入っています。プログラマーは通常この engine に対して命令をしたり結果を受け取ったりしているわけです。V8 (NodeJS, Chrome, Opera を動かしているエンジン), SpiderMonkey (Firefox), JSCore (Safari), Chakra (Edge) といったエンジンがこういった作業をしており、そしてこのエンジンの作業こそが JavaScript をインタープリタ型言語としてあらしめているのです。
この一連のプロセスにおいて 1-2 step はあなたが指定した型を利用しますが、step 3 は利用しません。このことは繰り返し言及する価値のある重要な事柄です。TSC があなたの TypeScript コードを JavaScript コードに compile する際には全く、型をチェックしません。つまり、あなたが書いた型に関する部分は compile によって生成されたコードには一切影響を与えないということです。その型は typecheck の際にのみ用いられるのです。この特徴のおかげで、アプリケーションを破壊することなく、プログラムの型を更新したり改善したりすることが可能なのです。
Type System
現代的なプログラム言語はそれぞれ異なった type system を持っています。
Type System とは:Typechecker があなたの書いたプログラムに型を与える際の規則のこと
大きく分けて二種類の type system が存在します。一つは、compiler に対して全ての型を明示的に伝える必要がある type system で、もう一つは compiler が型に関することを自動的に推測してくれる type system です。どちらの手法にも利点と欠点があります。
TypeScript は両方の type system に影響を受けており、明示的に型を annotate = 指定することもできますし、TypeScript に推測させることもできます。
明示的に TypeScript に型を伝えるためには annotation を使用します。annotation は次の形式で記述します。
「value: type」
こう書くことで typescriptchecker に次のように伝えたことになります。
「ここに value があるのがわかるよね。この value の型は type だよ。」
ではいくつか annotation の例を見ていきましょう。(後に続くコメントは。TypeScript が実際にどのような型として推測結果を出したかを示しています)
let a: number = 1 // a is a number
let b: string = 'hello' // b is a string
let c: boolean[] = [true, false] // c is an array of booleans
また TypeScript に型推測を完全に任せることもできます。その場合は、annotation を完全に外してしまうことで TypeScript に作業をさせることができます。
let a = 1 // a is a number
let b = 'hello' // b is a string
let c = [true, false] // c is an array of booleans
この結果を見ると如何に TypeScript がうまいこと型を推測してくれるかがよくわかりますね。Annotation を外しても、付けていた時と同じ推測結果が得られました。この本全体を通して annotation は不可欠な場合にだけ使うことにし、可能であれば TypeScript に推測の魔法を使ってもらうことにしましょう。
ちょっとしたメモ:一般的にいって TypeScript に出来るだけ型を推測させる方が良いスタイルと言えるでしょう。型を明示するのは最小限の方がいいでしょう。
TypeScript と JavaScript の type system の違い
TypeScript の type system をさらに深く見ていくことにしましょう。また TypeScript と JavaScript の type system を比較もしてみましょう。以下の表はそれをざっくり示したものです。TypeScript と JavaScript の型システムの違いを理解することが、TypeScript のどのように動いているのかという概念を把握する上での鍵です。
型はどのように bound されるか JavaScript: 動的に TypeScript: 静的に
型が自動で変換されるか JavaScript: Yes TypeScript: No(ほとんどの場合においては)
型はいつチェックされるか JavaScript: runtime 時に TypeScript: compole 時に
エラーが発覚するのはいつか JavaScript: runtime 時に(ほとんどの場合においては) TypeScript: compole 時に(ほとんどの場合においては)
型はどのように bound されるか
JavaScript においては動的に型が bound されるということはつまり、型を判断するために実際に実行しないといけないということです。つまり、型がいった何なのかは、実行するまで JavaScript にはわからないということです。
TypeScript は gradually typed という種類の言語です。これはつまり、compile 時に全ての型が明確にわかっている方がより良い結果をもたらすものの、全ての型がわからなくても compile は出来るということです。例え全く型が示されていなくても TypeScript は自ら型を推測し、ある程度ミスを見つけてくれますが、完全に型を把握できていな状態ではかなりの誤りを見逃してもしまいます。
この Gradual typing は、全く型が示されていない JavaScript によるレガシーコードを TypeScript へと移行させる際に効果を発揮します。しかしそのマイグレーションの途中でない限りは、型が 100% 明確になっている状態を目指すべきです。それが本書の基本方針です。
型が自動的に変換されるというのはどういうこと?
JavaScript は weakly typed という種類の言語なので、数値と配列を足すといった問題があることを命じても、様々な規則を用いて、本当はあなたがどうしたかったのかを推測します。例えば 3 + [1]
を JavaScript に試しに評価させることにしましょう。
- JavaScript は 3 が数値で
[1]
は配列であると認識する。 +
をつかっているのだから、この二つを連結したいはずだ、と推測する- 暗黙的に 3 を文字列に変換し、"3" を生成する
- 何目的に
[1]
を文字列に変換し、"1" を生成する - この二つの文字列を連結し、"31" を得る
同じことを明示的に JavaScript に実行させることもできます。(その倍には 1-3, 4 の step が飛ばされます)
(3).toString() + [1].toString() // evaluates to "31"
合理的だとは思えないことを実行させようとすると TypeScript はエラーを訴えますし、明示的にその意図を伝えてあげればその通りにしてくれます。この挙動を普通は期待するでしょう。正常な精神を持った人間が、数値と配列を足して、文字列を得たいと思うことなどあるでしょうか?(もちろん JavaScript 大魔導師のような人物で、その人生の全てをかけて地下の暗闇の中で JavaScript の全てを知るためにコーディングし続けたいような人もいるとは思いますが)
こういった類の暗示的な変換は、ソースコードをみてエラーの原因を見つけることを非常に困難にしてしまいます。これがほとんど全ての JavaScript エンジニアの苦しみの根源なのです。各エンジニアが自分の仕事を終わらせることを困難にするだけではなく、集団開発においてコードをスケールさせることも当然難しくしてしまいます。なぜなら全てのエンジニアに、このような暗示的な変換のルールの理解を強制するからです。
どう考えても、型を変換したいのであれば、明示的に示せばいいだけですよね。
type check はいつ行われる?
JavaScript は基本的にあなたがどんな型を与えたのかを気にせずに、あなたの意図を最大限慮り、あなたの期待しているであろう型へと変換します。
TypeScript はそうではなく、コンパイル時にコードをチェックするので、今まで見てきたようなエラーを引き出すために、実際に実行をする必要はありません。TypeScript は statically analyze = 静的に解析をおこなうことで、エラーを見つけ、実行するより前にそれを示してくれます。コンパイルが失敗したら、それはあなたがなんらかのミスをした証であり、実行する前にそのミスを修正することができます。
TypeScript のための拡張プラグインが各種コードエディターには用意されているのでそれを用いることで、コードをタイプした瞬間にエラーがあればエディター上に赤い波線で示されます。この機能によってコードを書き、エラーを見つけ、それを修正するという一連の作業の速度が飛躍的に向上します。
エラーに直面するのはいつ?
JavaScript が exeption を投げたり、暗黙的な型変換をおこなうのは、runtime の実行時においてです。つまり、何か無効な動作をプログラムにさせたことを検知するためには、実際にコードを実行しなくてはいけないということです。この検知が行われる一番いい状況は、unit test の中で検知される場合でしょう。最悪なのは、ユーザーからお怒りのメールが届く場合です。
TypeScript の場合は、シンタックスに関するエラーと型に関連するエラーについて、コンパイル時に投げられます。より現実的にはコードエディターに入力した瞬間にエラーが表示されます。この体験に感動することでしょう。特に incrementally compiled statically typed 言語を触ったことがない人にとっては。(incrementally compiled statically typed language とは、コードに変更があった場合に、変更部分だけを迅速に再コンパイルする言語。コード全体を再コンパイルするのではない。)
ただし、全てのエラーを TypeScript がコンパイル時に見つけれるわけではありません。例えば stack overflows, broken network connections, 不正な形式の user input などはコンパイル時に見つけることができません。これは残念ながら runtime 実行時に exception が投げられます。つまり TypeScript はエラーの大半については compile 時に投げ、残りの部分に関しては純粋な JavaScript の世界において実行時にエラーを投げるということになります。
Code Editor の設定
TypeScript compiler と type system がどのようなものか直感的に理解できたと思うので、ここからは実際にエディターをセットアップしてコードそのものの世界に踏み入れていくことにしましょう。
まずはコードエディターをダウンロードしましょう。私は WebStorm を好んで使っています。なぜなら非常によい TypeScript 体験を提供してくれるからです。もちろん VSCode, Sublime Text, Atom, Vim 等々あなたの好きなものを使ってもらって全く問題ありません。ただしエンジニアは WebStorm のような IDE を好む傾向にあります。
TSC それ自体は TypeScript で書かれた command-line アプリケーションですので、これを実行するためには NodeJS を用意する必要があります。Node の公式サイトを参考に、セットアップしてあなたのマシーンで実行してください。
NodeJS には NPM という、dependencies を管理し、ビルド作業を編成するパッケージマネージャーがあります。まずはこれを使って TSC と TSLint(TypeScript のための linter)をインストールします。ターミナルを開いて新しいフォルダを作成し、新規 npm プロジェクトを作成しましょう。
# 新規フォルダを作成する
mkdir chapter-2
cd chapter-2
# 新規 NPM project を立ち上げる(その後色々聞かれますが適当にやっといてください)
npm init
# TSC, TSLint, type declarations for NodeJS をインストールする
npm install save-dev typescript tslint @types/node
tsconfig.json
全ての TypeScript プロジェクトは tsconfig.json ファイルをルートディレクトリに持たなくてはいけません。このファイルによって TypeScript はどのファイルをコンパイルすればいいか、コンパイルしたものをどのディレクトリに出力すればいいか、また出力する JavaScript をどのバージョンとするか、といったことを決定します。
まず tsconfig.json という新規ファイルをルートフォルダに作成し、それを開いて以下のような内容を入力します。
{
"compilerOptions" : {
"lib" : [ "es2015" ],
"module" : "commonjs" ,
"outDir" : "dist" ,
"sourceMap" : true ,
"strict" : true ,
"target" : "es2015"
},
"include" : [ "src" ]
}
ざっくりこれらのオプションが何を意味するのか見ていきましょう。
- include: TSC が TypeScript ファイルを探す際に見るフォルダの指定
- lib: コードを実行する環境に存在すると TSC が考えていい API の指定。例えば ES5 と指定すれば Function.prototype.bind が使えると TSC は考えますし、ES2015 と指定すれば Object.assign、DOM と指定すれば document.querySelector などが使える環境であると判断します。
- module: TSC がコンパイルする際にどの module system を使用するか(CommJS なのか SystmeJS なのか ES2015 なのか、もしくはその他のシステムなのか)
- outDir: TSC が生成した JavaScript コードをどのフォルダに出力するか
- strict: true にすると、invalid なコードをチェックする際に、なるべく厳しくチェックをおこなうようになる。その場合には、全てのコードが適切に型付けされている必要がある。本書では基本的にはこの設定を使っていく。
- target: TSC がどの JavaScript のバージョンにコンパイルして出力するかの設定(ES3, ES5, ES2015, ES2016 などがある)
他にもたくさんの option がありますし、バージョンが新しくなるにつれてかなり追加されます。ただしそこまでこの設定を大きく変えることは実際にはないでしょう。あるとすれば module bundler を変更した際に module や target の設定を変えるとか、"dom" を lib に追加して TypeScript を browser で実行する場合や、JavaScript を TypeScript にマイグレーションする際に strictness のレベルを変更するくらいでしょうか。option に関する完全な情報は公式サイトから得てください。
tsconfig.json を使って TSC の設定をすることによって、git のようなソース管理ツールで管理できるのが非常に便利ですが、TSC option の大半はコマンドラインから与えることもできます。npx tsc --help
と実行するとコマンドラインで使用できる option を確認することができます。
tslint.json
TSLint の設定のための tslint.json もプロジェクトに用意したほうがいいでしょう。コードスタイルに関する慣習を適応することができます。(tab なのか space なのか等)
メモ:TSLint を使用するのは強制ではありませんが、しかし強く推奨いたします。こうすることで coding style を一定に保つことができ、共同作業者とコードレビュー時にどのスタイルでコードを書くか議論する時間を省くことができます。
npx tslint --init
と実行することでデフォルトの TSLint 設定を作成することができます。これに対してあなたの望む設定を上書きすればいいでしょう。例えば次のようになります。
{
"defaultSeverity": "error",
"extends": ["tslint:recommended"],
"rules": {
"semicolon": false,
"trailing-comma": false
}
}
規則全体については TSLint のドキュメントを確認してください。カスタムルールを追加したり、ReactJS 用などの preset をインストールすることもできます。
index.ts
tsconfig.json と tslint.json を用意したので、次は src フォルダーを作って、ここに最初の TypeScript ファイルを作ることにしましょう。
mkdir src
touch src/index.ts
するとプロジェクト構造は以下のようになっているはずです。
root_directory/
├──node_modules/
├──src/
│ └──index.ts
├──package.json
├──tsconfig.json
└──tslint.json
src/index.ts を開いて次のコードを入力して下さい。
console.log('Hello TypeScript!')
そうしたらこの TypeScript コードをコンパイルして実行しましょう。
# Compile your TypeScript with TSC
npx tsc
# Run your code with NodeJS node
node dist/index.js
「Hello TypeScript!」
雛形を使わずに全て自分の手で最初の TypeScript プロジェクトを構築し、実行することができましたね。素晴らしい。
メモ:初回のレクチャーであったため全て自分の手で構築したほうが良いと考えましたが、次からは以下のようにしたほうが素早くプロジェクトを立ち上げることができるでしょう。
- ts-node をインストールしてこれを使うことで TypeScript のコンパイルと実行を一つのコマンドで済ませることができる
- typescript-node-starter といった雛形を使うことで、迅速に TypeScript プロジェクトを立ち上げる
練習問題
環境ができましたので、src/index.ts をエディターで開き、次のコードを入力してください。
const a = 1 + 2
const b = a + 3
const c = {
apple: a,
banana: b
}
const d = c.apple * 4
そうしたら各変数の上にコマンドを押しながらマウスをホバーさせてください。すると、TypeScript が各変数がどのような型になるかを推測したか、その結果を見ることができます。a は number、b は number、c は特定の shape の object、d は number と表示されますね。
では少しコードを変更して、以下のような現象を起こしてみましょう
- 何か invalid なことをして赤い波線を表示させましょう(つまり TypeError を起こさせるということです)
- TypeError の中身を読んで、理解しましょう
- TypeError を修正して、赤い波線が消えるのを確認しましょう