Ateam Lifestyle x cyma Advent Calendar 2018の初日は、株式会社エイチームライフスタイルの@suzuki_shが担当します。
本稿では「TypeScriptを使って開発をしてみたい」と思ってもらえるように、その魅力を紹介していきます。前半は「すこしJavaScriptを書いた事がある人」向けに、すぐにTypeScriptにキャッチアップする方法を紹介します。後半はちょっと踏み込んで「TypeScriptがあると何が嬉しくなるのか」を紹介します。本稿が、TypeScriptを使ったたのしい開発のキッカケになればと思います。
とくにフロントエンドへ携わる人に広くTypeScriptをさわってほしいため、前半はWebデザイナーの方でも読めるようやさしい内容にします。しかし後半の内容はエンジニアでないと難しいかもしれません。ご容赦ください。
環境
下記の環境で動作を確認しています。
- Windows 10 Pro 1803 Windows Subsystem for Linux / Ubuntu 16.04.4 LTS
- 恐らくMacでも動きます
- JavaScript: Node.js 11.2.0(99% ECMAScript5)
- TypeScript: 3.1.6
- ただしTypeScript Playgroundについてはバージョン不詳
1: TypeScriptにキャッチアップする
すべてのJavaScriptはTypeScriptになる
TypeScriptはJavaScriptのスーパーセットです。どういうことかと言うと、すべてのJavaScriptの文法は、TypeScriptでも使うことができます。
こちらはJavaScriptのプログラムです。
const str = 'Hello, JavaScript!';
console.log(str);
こちらはTypeScriptのプログラムです。
const str = 'Hello, TypeScript!';
console.log(str);
まったく同じですね。
このように、JavaScriptを書くことができればすぐにTypeScriptを書き始めることができます。
JavaScriptを書ける人ならば、TypeScriptは怖くありません。
ではTypeScriptは何が違うのかというと、JavaScriptに型システム(Type System)を導入して拡張したプログラミング言語が"Type"Scriptです。
型については後述しますが、型の記述はオプションです。上記のように、型を書かなくても動作します。
たとえば先のプログラムは、型を明示的に記述して、このように書くこともできます。
const str: string = 'Hello, TypeScript!';
console.log(str);
: string
の部分が型の記述です。
AltJS
TypeScriptは、JavaScriptに変換(complie)することで、JavaScriptとして実行されます。このようなプログラミング言語を総称してAltJSと呼ぶことがあります。
新しいブラウザはもちろん古いブラウザでも、あるいはサーバ上のNode.jsでも、JavaScriptが動く場所であればどこでも、TypeScriptを使って書いたプログラムを動かすことができます。2018年現在JavaScriptは広くいろいろな場所で使われていますが、TypeScriptはJavaScriptと同じところで利用できます。
Playground
TypeScriptには公式のPlayground(遊び場)が用意されており、アクセスするだけですぐTypeScriptを使ってみる事ができます。
https://www.typescriptlang.org/play/index.html
左側のTypeScript入力エリアに、下記のプログラムを書いてみてください。
const str: string = "Hello, TypeScript";
window.alert(str);
右側のJavaScriptエリアには、リアルタイムで下記のプログラムが表示されたと思います。
const str = "Hello, TypeScript";
window.alert(str);
これが、TypeScriptから変換された結果のJavaScriptになります。
: string
というTypeScript独自の記法が削除された、普通のJavaScriptになっています。
「Run」ボタンを押すとブラウザで新しいタブが開いて、JavaScriptプログラムが実行されます。
以降のプログラムは、このPlaygroundで実行しながら進めていきます。
静的型付け
変数にはstring
(文字列)やnumber
(数値)といった、型を指定できます。
let str: string;
let num: number;
str = "Hello, TypeScript";
num = "Hello, TypeScript"; // エラー
このように、number
で定義された変数に、"Hello, TypeScript"
というstring
が代入されると、事前の型チェックでエラーが発生します。
PlayGround上でも確認できます。
型のチェックがあることによって、プログラムが予期しない挙動をしないかどうか、事前に確認できます。
また、関数の引数と返り値にも型を指定できます。
function sayHello(person: string): string {
return `こんにちは、${person}さん`;
}
function sayAge(age: number): string {
return `わたしは${age}歳です`;
}
const hello: string = sayHello("鈴木");
const hello2: number = sayHello("鈴木"); // 返り値の型がNumberではないのでエラー
const hello3: string = sayHello(123); // 引数の型がStringではないのでエラー
sayAge(18);
sayAge(hello); // 引数の型がNumberではないのでエラー
さらに型推論という仕組みがあり、明示的に指定されなかった型についても自動的にチェックされます。
function sayHello(person: string) {
return `こんにちは、${person}さん`;
}
let age = 18;
age = sayHello('鈴木'); // number型の変数にstring型を代入しているので、エラー
sayHello()
の返り値はstring
であり、変数age
はnumber
であると読み取ってくれます。
そのため型の指定を何も書いていなくても、型チェックが行われ、「number
型の変数にstring
型を代入している」と判断され、エラーが発生します。
大切なのは、 型の指定を省略しても型チェックの恩恵をうけることができる という事です。
つまり型を省略した通常のJavaScriptと同じ記法でも、十分にTypeScriptを活用できます。JavaScriptを書ける人ならば、TypeScriptは怖くありません。
Visual Studio Code (VS Code)
もちろんVim、Emacs、WebStormなど、他のエディターでもTypeScriptは開発できます。
ですがVS CodeでTypeScriptを書くならば、プラグイン等は一切不要です。インストールしたそばからシンタックスハイライトはもちろん、静的型チェックや強力な補完機能などの恩恵を、すぐに受けることができます。
こういった機能を活用すると、TypeScriptに詳しくなくても生産性高くコードを書くことができます。
TypeScriptをローカルで実行する
PlaygroundではなくローカルでTypeScriptを実行するには、JavaScriptに変換する必要があります。
まず、下記コマンドでTypeScriptをローカルにインストールします。
$ npm install --global typescript
TypeScriptを書いたら、tsc
というコマンドでコンパイルします。
const str: string = "Hello, TypeScript";
console.log(str);
$ tsc index.ts
$ node index.js
Hello, TypeScript
と表示されたら成功です。
どうやって導入するか
TypeScriptをプロダクトに導入するには、コンパイルした結果のJavaScriptを実行したい所へ配置(Deploy)するだけです。
たとえばWebフロントエンドにおいて、webpack等のパッケージングツールが導入されている場合は、そこでTypeScriptのコンパイルを行うことができます。
なお2018年11月時点でTypeScript公式ドキュメントに書いてあるwebpackの導入手順はすこし古いものになっています。webpackのconfigファイルなどを書く場合は、GitHubに上がっているMergeRequestの内容などを参考にしましょう。
https://github.com/Microsoft/TypeScript-Handbook/pull/765
まとめ
以上の知識があれば、ちょっとJavaScriptを知っているだけでも最低限のTypeScriptを読み書きができると思います。JavaScriptを書ける人ならば、TypeScriptは怖くありません。
TypeScriptには他にも、クラス・インターフェース定義やジェネリクスなど、強力で豊富な機能をたくさん備えています。しかし、それらを説明するには「クラスベースのオブジェクト指向」についての理解が必要なので、本稿では割愛します。
2: TypeScriptを使うと何が嬉しいのか
ここからはTypeScriptを導入することのメリットを説明します。
TypeScriptの最大の特徴は、その名にTypeとあるとおり**「静的型付け」**です。
では、静的型付けがあると何が嬉しいのでしょうか。
"正しい"プログラムを書く
私は、「型システムを使えば、それだけで必ず"正しい"プログラムが書けるのではないか?」と思いました。(ここで言う"正しさ"とは、プログラムに不具合が無く意図通りの挙動をする事とします。書き方が美しかったり、動作が早かったりすることは含みません)
型システムが表している"正しさ"とは何なのかを、本稿の残りを使って調べていきます。
そもそも、型とは何でしょうか。「型システム入門」にはこうあります。
型システムとは、プログラムの各部分を、それが計算する値の種類に沿って分類することにより、プログラムがある種の振る舞いを起こさないことを保証する、計算量的に扱いやすい構文的手法である。
一方、プログラムの正しさを説明する方法には「テスト」があります。
「ソフトウェア・テストの技法 第2版」にはこうあります。
ソフトウェア・テストとは、コンピュータ・コード外とされたように動作し意図されないことは全て実行しないように設計されていることを検証するように設計されたプロセスである。
以下、単体テストと静的型チェックとを比べてみます。
正しさをプログラムで説明する
ここで例として、下記の簡単な数学的命題について、"正しさ"をプログラムで表現することに取り組んでみます。
任意の実数 x に対して、x + 1 + 1 = x + 2 が常に成り立つ。
もうすこしプログラムらしく言うと「$x$ が実数ならば、ふたつの関数 $(x + 1 + 1)$ と $(x + 2)$ が、同値」という事を確認します。
例A: JavaScript
まずは、TypeScriptではない普通のJavaScriptでの挙動を確認します。
function plus_1_plus_1(x) {
return x + 1 + 1;
}
function plus_2(x) {
return x + 2;
}
// Test
console.log(plus_1_plus_1(1) == plus_2(1)); // true
console.log(plus_1_plus_1(2) == plus_2(2)); // true
console.log(plus_1_plus_1(3) == plus_2(3)); // true
このように、xに適当な値を代入していって実行結果を確認するテストによって、ふたつの関数の返す値が正しいことを確認できます。
しかし、この方法には2つの問題があります。
- xが1,2,3のときには成り立つが、すべてのxに対して成り立つかどうかは分からない
- 予期しない値が入力された場合、どのような挙動をするのか分からない(文字列、オブジェクト、null等)
実際に、xが文字列だと等式は成り立ちません。
console.log(plus_1_plus_1("hoge") == plus_2("hoge")); // "hoge11"と"hoge2"になるので、false
例B: TypeScriptプリミティブ型
TypeScriptで、型定義をしながらプログラムを書いてみます。
function plus_1_plus_1(x: number) {
return x + 1 + 1;
}
function plus_2(x: number) {
return x + 2;
}
// Test
console.log(plus_1_plus_1(1) == plus_2(1)); // true
console.log(plus_1_plus_1(2) == plus_2(2)); // true
console.log(plus_1_plus_1(3) == plus_2(3)); // true
TypeScriptにすることにより、ふたつの関数において「引数がnumberである」「返り値がnumberである」ことが、テストコードを書くこと無く保証されました。
まず「引数がnumberである」ことによって、JavaScriptの例Aにおける問題がひとつ解決しています。
引数の型にnumberを指定することで、この関数には「数値以外を渡されることがない」ことが保証されています。
実際、xに文字列を代入すると、型チェックによりエラーが表示されます。
console.log(plus_1_plus_1("hoge") == plus_2("hoge")); // エラー
TypeScriptで静的型チェックがあることにより、「予期せぬ引数を渡される」可能性がなくなったため、JavaScriptよりも"正しさ"が上がったといえます。
次に「返り値がnumberである」事により、少なくとも「x + 1 + 1」と「x + 2」は配列や文字列だったりすることはなく、同じnumberという型に収まる事が保証されました。
ただしnumberにはたくさんの数値が含まれます。x + 1 + 1も、x + 2も、ただnumberに属すると言うだけでこの2者が一致するかどうかまでは分かりません。
もし下記のような、numberを返すplus_3()
関数があった場合、plus_1_plus_1()
もplus_2()
も、同じnumberという事になってしまいます。
function plus_3(x: number) {
return x + 3;
}
逆に言えば、plus_3()
と、plus_1_plus_1()
とplus_2()
とを区別するアイデアがあれば、静的型チェックを使ってより"正しい"プログラムを書けるかもしれません。
例C: TypeScriptドメイン固有型
返り値がnumberという型では広すぎます。そこで、もっと型の範囲を"狭める"ことで、プログラムの正しさを向上できるのではないでしょうか。
独自に定義したTwoAddX
という型を追加してみます。
class TwoAddX {
constructor(private num: number){};
}
function plus_1_plus_1(x: number) {
return new TwoAddX(x + 1 + 1);
}
function plus_2(x: number) {
return new TwoAddX(x + 2);
}
いかなるxにおいても、ふたつの関数はTwoAddX
という型を返す、という事が、テストコード無しに示されました。
たとえばplus_3()
という別の関数があっても、返り値がTwoAddX
型でなければ、それと異なる事が保証されています。
これこそが完全に正しいプログラムではないでしょうか?
まだ"正しさ"を保証しきれていない所があります。plus_1_plus_1()
の定義をよく見てみましょう。
function plus_1_plus_1(x: number) {
return new TwoAddX(x + 1 + 1);
}
「この関数の返り値はTwoAddX
型である」という命題は、たしかに必ず成り立ちます。しかし、「返り値がxに2を足したもの」なのかどうかは証明されておらず、実際に計算してみないと分かりません。
ここで、すこし状況を俯瞰するために新しい観点を導入します。数学で言う命題と証明とは、それぞれプログラムにおける型と関数とに対応させることができます。これを突き詰めた カリー・ハワード同型対応 という関係によって、数学的な証明をプログラムで表現できる事が知られています。ここではその詳細には触れませんが、ざっくりと対応させてみます。
例Cのプログラムは、数学にたとえると何を示しているのでしょうか。
- 「
plus_1_plus_1()
の返り値はTwoAddX
である」という 命題 は、TypeScriptの静的型チェックによって示されている -
plus_1_plus_1()
がxに2を足したものなのかどうかは、型システムでは 証明 されていない(テストでないと示せない)
つまりTypeScriptでテストを書かずに型システムを使うとは、証明されていない命題を使うようなものです。裏を返せば、テストコードによって、型システムが示しきれない証明の部分、「プログラムが正しい挙動をすること」を、補うことができます。
console.log(plus_1_plus_1(1) == plus_2(1)); // true
console.log(plus_1_plus_1(2) == plus_2(2)); // true
console.log(plus_1_plus_1(3) == plus_2(3)); // true
ここまでで、テストと型チェックの違いについてまとめます。
静的型チェックは演繹的であり、テストにおいてよくある「テストケースの漏れ」のような事態が発生することはありません。例Bにおいて、引数にnumber
という型を追加することで、string
型が想定できていないという例外を排除できました。またTwoAddX
という独自の型を定義することで、予期しない返り値の型が帰ってくる可能性も除外できました。
一方で、プログラムが想定した挙動をするかどうかというのは、静的型チェックでは調べることはできませんでした。それを保証するために、A~Cのいずれの例においてもテストコードが必要となりました。
手法 | 論理 | 対象 | 数学における対応 |
---|---|---|---|
静的型チェック | 演繹的 | 関数の入出力の種類 | 命題 |
テスト | 帰納的 | (おもに)関数や処理の結果 | (証明※1) |
型は強力で、記法もシンプルですが、適用できる範囲は限られます。静的型チェックを上手く使って「テストしなければいけない範囲を狭める」事が、"正しい"プログラムを書く上で重要だと思います。
以上より、TypeScriptで静的型チェックが使えることで、プログラムの"正しさ"が一定の向上をすると言えます。
なぜTypeScriptなのか
先ほど「TypeScriptでテストを書かずに型システムを使うとは、証明されていない命題を使うようなもの」と書きました。
数学の証明問題において、命題が証明されている事が自明ならば、それらを組み合わせて新しい証明をよりスムーズに進めることができます。歴代の数学者たちがたくさんの定理を証明してきた事によって、あたらしい定理の証明はどんどん楽になり、より複雑な証明に取り組むことを可能にしました。
プログラムにおいても、「十分にテストされた"正しい"関数がたくさんある状況」が作られたならば、そこに型システムがあることによって関数を"正しく"使うことができるといえます。
「十分にテストされた"正しい"関数がたくさんある状況」というのは、何でしょうか?JavaScriptにおいてnpmパッケージが豊富にあることは、その条件を満たすと言えます。
Modulecounts.comによると、インターネット上に公開されている利用可能なnpmパッケージ数の豊富さは、他の言語と比べて群を抜いています。これらのパッケージが本当に十分テストされているか、TypeScriptによる型定義が用意されているのかどうかは、定かではありません。しかし母集団が圧倒的に多いことは事実です。TypeScriptの静的型チェックがあることによって、これらのパッケージの恩恵をより大きく受けていくことができます。
TypeScriptにおける「大規模」
TypeScriptは、よく「大規模なアプリケーションで効果的」のように言われます。TypeScript公式サイトにも「Strong tools for large apps」とあります。
ですが以上の話を踏まえると、大規模というのは利用するnpmパッケージも含めて、の話だと私は思いました。たとえばAzure Functions、AWS Lambdaのように、自身がコーディングする規模は狭くても、豊富なnpmパッケージを活用するならばTypeScriptによって恩恵を引き出していくことができると思いました。
まとめ
- "正しい"プログラムが書きたい
- TypeScriptを使って静的型チェックが付くと、"正しく"関数を使うことができる
- npmパッケージとして、"正しい"関数がたくさん提供されている
- よってTypeScriptを使うと、(JavaScriptよりも)"正しい"プログラムが書きやすくなる
ここまでTypeScriptが使いやすいこと、TypeScriptを使って嬉しくなることを説明してきましたが、もちろんTypeScriptにもデメリットはあります。たとえば、ブラウザでもサーバ上のNode.jsでも、JavaScriptがそのまま動く環境下において、わざわざコンパイルをしなければTypeScriptのプログラムを実行することはできません。
メリットがデメリットを上回るような状況であれば、JavaScriptよりもTypeScriptを使ったほうが嬉しくなると思います。
参考文献
- 速習TypeScript | 山田祥寛 著
- 型システム入門 プログラミング言語と型の理論 | Pierce,Benjamin C.著
- 型付けを活用してテストを減らす:静的型を使ったTDD Part 1 | POSTD
- グッド・マス ギークのための数・論理・計算機科学 | MarkC. Chu-Carroll著
- プリミティブ型よりドメイン固有の型を | プログラマが知るべき97のこと
- カリー・ハワード同型対応入門 | 哲学基礎文化学ゼミナールII - 京都大学OCW
Ateam Lifestyle x cyma Advent Calendar 2018、明日は @ihsiek に書いてもらう予定です。お楽しみに!
エイチームグループでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
https://www.a-tm.co.jp/recruit/
-
数学的帰納法のようにすべてのテストケースを網羅できた場合のみ、テストは演繹的な証明として成立する ↩