TypeScript再入門 ― 「がんばらないTypeScript」で、JavaScriptを“柔らかい”静的型付き言語に

JavaScriptプロジェクトでTypeScriptを導入する際には、“柔らかい”静的型付き言語とするのがおすすめです。藤吾郎(gfx)さんがまとめた「がんばらないTypeScript」のガイドラインです。

TypeScript再入門 ― 「がんばらないTypeScript」で、JavaScriptを“柔らかい”静的型付き言語に

TypeScriptは、すべてのJavaScriptプロジェクトで採用する価値のある技術です。TypeScriptとこれに対応したエディタを導入することで、補完や型ベースの整合性のチェックにより、すべてのプロジェクトで生産性が上がります。またリファクタリングも容易になるので、長期あるいは大規模なプロジェクトでも品質を保ちやすくなります。

この記事では、TypeScriptについて最低限の知識とともに、サクッと(どちらかというと既存のプロジェクトに)導入するためのノウハウや手順を、筆者@__gfx__の経験もあわせて解説していきます。

ただし、TypeScriptのユースケースは、Webフロントエンド、モバイルアプリ、サーバーサイドアプリなど多岐にわたります。そこでこの記事では、以下のようなプロジェクトを想定して、設定などを具体的に見ていきます。

  • Webフロントエンドのビルドにwebpackを使う
  • ES modulesまたはCommonJS準拠のモジュールシステムを使う

※本記事は、TypeScript v3.4(2019年3月リリース)に基づいています。

TypeScriptとは何か? がんばらないTypeScriptとは何か?

あらためて「TypeScriptとは何か?」から始めましょう。TypeScriptは、静的型付きプログラミング言語です。特徴としては、ほとんどの言語機能をJavaScriptから継承していることです。TypeScriptのコードはJavaScriptに変換(コンパイル1され、最終的にJavaScriptエンジンで実行されます。

これは重要なことなので何度も繰り返しますが、実際、TypeScriptはほとんどJavaScriptです。例えば、公式サイトのうたい文句は“JavaScript that scales”です。“scales”というのは、ここでは「大規模開発に耐えられる」といった意味ですから、「大規模開発に耐えられるJavaScript」ということです。

JavaScriptにコンパイルされる言語はたくさんありますが、TypeScriptはコンパイル前と後のコードにほとんど違いがありません。「TypeScriptはほとんどJavaScriptである」というのは文字通りの意味で、JavaScriptの知識はすべてそのままTypeScriptに応用できます。

これに対して、CoffeeScriptやDartといったJavaSciptと大きく異なる言語機能を持つaltJSでは、コンパイル後のコードは、元のコードとも自然なJavaScriptとも異なるものになります。その良し悪しはともかく、JavaScriptと異なる言語機能を持つこれらのaltJSには、JavaScriptとは異なる知識体系が必要ということです。

ところで、一般的には、静的型付き言語は動的型付き言語より高速であるといわれます。つまり、静的な型(型注釈)はCPUのための型でもあると考えられています。しかし、TypeScriptはJavaScriptと実行速度が変わりません。つまり、TypeScriptの型注釈は、人にとってはコメントであり、またエディタなどのツールにとっては補完などのコーディングサポートに使われる情報でもありますが、実行には何の影響もないのです。

TypeScript導入の可否

ところで2019年のいま、TypeScriptは導入すべきでしょうか。これについては「導入すれば得るものが多い」という評価がほぼ定まっていると考えてよいでしょう。「『TypeScriptを導入すべきか』で悩む時代は既に終わっている」という意見もあります。

JavaScriptプロジェクトへの導入も、コンパイルを通すまでなら、早ければ数分で済みます。一通り動作確認して完成させるまででも、数日程度で済むはずです。

試しに、この記事のためReact公式サイトで紹介されているReactアプリをいくつかTypeScript化してみました。作業時間としては、インストールの待ち時間を除けば、それぞれ10分程度で変換して動作確認までできました。

アプリ 差分(プルリクエスト) wc -l
Calculator gfx/calculator/pull/1 461行
Pokedex gfx/pokedex/pull/1 267行

もっとも、多少のハマりどころがないわけではありません。特に、TypeScriptを導入する際の設定は知識が必要です。また、どのようにTypeScriptが実行されるかという一連の流れの理解につまずく人が多いようです。本記事では、そういったハマりどころを集中して解説していこうと思います。

難しい型システムをマスターしなくてもTypeScriptで開発できる

TypeScriptは、JavaScriptに静的型という大いなる力を与える言語です。そしてその「静的型の堅さ」もかなり自由に設定できます。strictにするオプションをすべて有効にして「堅い」静的型付き言語として振る舞うこともできれば、その逆にJavaScriptに毛が生えた程度の「柔らかい」静的型付き言語として振る舞えもします。

ところで、TypeScriptは型や設定が複雑で難しく学習コストが高いという意見をよく目にします。実際のところ、TypeScriptの型システムをマスターするのが難しいというのは事実です。設定ファイル(tsconfig.json)の項目も多く、しかも「静的型の堅さ」について最初から適切な判断をしなければならないような気がして、敷居が高く感じてしまうのも仕方がありません。

しかし、型システムをマスターしなければTypeScriptで開発できないわけではありません。複雑な型演算2は、アプリケーションを書くときに必要になることはほとんどありません。またほかの多くの静的型付き言語と異なり、型エラーで困ったときはいつでもanyで握りつぶせます。動くことが分かっているコードで遭遇する型エラーを握りつぶすのは、特にJavaScriptをTypeScriptに置き換えるフェーズでは、ためらわずにやる方がよいと思っています。

設定ファイルについても同じように、最初からすべての型チェックのオプションを有効にするべきとは思いません。いくつかの型チェックは、少なくともTypeScriptに不慣れな間は、無効にする方が生産性が上がるはずです。

「がんばらないTypeScript」というガイドライン

この記事では、以下のガイドラインに適合するTypeScriptのスタイルを「がんばらないTypeScript」スタイルと定義します。

設定ファイルをがんばらない
  • tsconfig.jsonではオプトインの型チェックオプションを任意で無効化してよいものとする
  • 特にnoImplicityAnyは無効化を奨励する
型付けをがんばらない
  • 型注釈はあとから足せばよいと割り切る
  • コードにおいてはいつでもanyで型エラーを握りつぶしてよい
型定義ファイルをがんばらない
  • 型定義の提供されていないライブラリは型がないまま使う
  • DefinitelyTyped@typesはしばしば「まともなものがあればラッキー」くらいに割り切る

それぞれについての詳細は後述します。

この「がんばらない」は、「自分でもがんばらないし、チームメイトにもがんばりを求めない」、例えばコードレビューでは厳密な型注釈を求めないということです。チームで運用するとなれば、そのチームでコンセンサスを得る必要もあるでしょう。

「がんばらないTypeScript」は、TypeScriptの設定や型の厳格な運用に時間を使うのではなく、プロダクトの開発に時間を使うべきという思想です。TypeScriptに慣れていないときは特にそうで、型付けは慣れてからする方がずっと少ない時間で正確にできるはずです。

そして、たとえTypeScriptについてがんばらないとしても、TypeScriptには型推論があるため型チェックよる恩恵は十分あります。TypeScriptの恩恵をほどほどに得つつプロダクト開発の生産性を上げていこう、というのが「がんばらないTypeScript」の目指す道です。

なお、この「がんばらないTypeScript」というガイドラインの名称は、@t_snzkさんのブログ記事から拝借しました。この記事の「がんばらないTypeScript」の定義も、基本的にはこのエントリのものと互換性があります。

「がんばらないTypeScript」で得られるもの

ところで「がんばらないTypeScript」の話をすると、よく「それではTypeScriptを導入する意味がないのではないか」といわれます。しかし、もちろんそんなことはありません。

TypeScriptは、すべてのstrict系オプションを無効にした状態でさえ、JavaScriptよりはずっと厳格です。例えば、以下のコードはJavaScriptでは実行可能で無意味な値(NaN)を出力しますが、TypeScriptだと最も緩いデフォルトの設定でもコンパイルは通りません。

// foo-increments.ts
let foo = "foo";
foo++;
console.log(foo);

コンパイルすると、次のようなエラーになります。

$ npx ts-node foo-increments.ts
⨯ Unable to compile TypeScript:
add.ts:4:1 - error TS2356: An arithmetic operand must be of type 'any', 'number', 'bigint' or an enum type.

4 foo++;
  ~~~

この出力は「数値演算はanynumberbigintまたはenum typeに対してのみ行えます」という意味です。

これは、TypeScriptだとlet foo = "foo"と宣言したとき変数fooが型推論によって文字列型になり、文字列型にインクリメント演算子(++)は適用できないためです。

このほか、ECMAScriptの標準ライブラリの型定義や標準DOM APIの型定義がもともと提供されていることもあり、「がんばらないTypeScript」の設定のもとで必須でない型注釈をすべて省略したとしても、組み込み型のための型チェックとコード補完により、JavaScriptよりはるかに開発しやすいことでしょう。

もちろん、TypeScriptに習熟するにつれて、徐々に厳格にしていくことは可能ですし、そうした方がよいとは思います。しかし、最初からTypeScriptをマスターする必要はありませんし、またマスターしていなくても、型チェックの恩恵は十分にあります。

TypeScriptによるWebアプリケーションの開発

それでは実際に、TypeScriptの設定を見てみます。最初に説明したように、次のような条件のWebアプリを想定しています。

  • Webフロントエンドのビルドにwebpackを使う
  • ES modulesまたはCommonJS準拠のモジュールシステムを使う

これらに必要なツールチェインとTypeScriptの考え方を見ていきます。

TypeScriptのコンパイラ

TypeScriptのコンパイラは、npmモジュールとして配布されています。TypeScriptコンパイラはTypeScriptで書かれており、TypeScript APIを使ったサードパーティ製のカスタムコンパイラも数多くあります。

このモジュールにはコンパイラであるtscコマンドと、language serviceを提供するtsserverコマンドが入っています。tsserverはエディタなどのためのlanguage service3を利用するツールが使うものです。

また、ちょっとしたTypeScriptコードをすぐ試したいときやテストの実行などに便利なts-nodeというコマンドがnpmモジュールとして提供されています。ts-nodeはTypeScriptのカスタムコンパイラです。

2ts-node - npm

使用例は次のようになります。

$ npx ts-node -e 'let a = "foo"; a++'
[eval].ts:1:16 - error TS2356: An arithmetic operand must be of type 'any', 'number', 'bigint' or an enum type.

1 let a = "foo"; a++
                 ~

webpackでTypeScriptを使うときは、ts-loaderを利用します。これもTypeScript APIを使ったカスタムコンパイラです。

3ts-loader - npm

エディタはlanguage serviceをサポートしたものを

エディタは、必ずTypeScriptのlanguage serviceをサポートしたものを使ってください。TypeScriptのlanguage serviceは非常に強力なので、これが使えないとTypeScriptを使う利点は半減します。

おすすめは、Visual Studio Code(vscode)です。

筆者は、RubyMine(IntelliJ IDEA系列のIDE)もよく使います。

このいずれも追加のプラグインなしで、TypeScriptのlanguage serviceを利用できます。

シンプルなTypeScript環境でスクリプトを実行してみる

webpackの設定に入る前に、まずシンプルなTypeScript実行環境を作って試しましょう。

手順は次のようになります。リポジトリ名はhello-typescriptです。

mkdir hello-typescript
cd hello-typescript
npm init -y
npm add -D typescript ts-node @types/node core-js
npx tsc --init
echo 'node_modules/\n*.js\npackage-lock.json' > .gitignore
git init && git add . && git commit -m "initial commit"

tsc --initは、TypeScriptの設定ファイルであるtsconfig.jsonを生成します。

このデフォルトで生成されるtsconfig.json自体、コメントが豊富で4、情報量が多いのですが、載っていない重要なオプションもあるので、TypeScriptのコンパイラオプションも眺めるとよいでしょう。TypeScriptのアップデートに伴いtsc --initの出力もアップデートされてきているため、新しいプロジェクトを始めるときはtsc --initを確認するといいでしょう。

これでひとまず環境はで出来上がります。次のようなhello.tsを用意して(これはJavaScriptとしても妥当なスクリプトです)

// hello.ts
console.log("Hello, TypeScript!");

ts-nodeで実行してみましょう。

$ npx ts-node hello.ts
Hello, TypeScript!

簡単ですね。

とはいえ、ここにES modules/CommonJSなどで外部モジュールを読み込むと少し複雑になります。

例えば、次のようなNodeJSのfsモジュールを使うスクリプトを考えます。

import * as fs from "fs";
// このファイル自身を読み込んで表示する
console.log(fs.readFileSync(__filename).toString());

このときimport文やrequire式は、TypeScriptとNodeJSそれぞれで独立して評価されます。つまり、TypeScript自身もモジュールを探して(module resolution)、存在しなければコンパイルエラーにします。

ここでは、@types/nodeという型定義モジュールが、fsモジュールの型定義やグローバル変数__filenameを提供します。試しにnpm remove @types/nodeをしたあとnpx tscとコンパイルだけをしてみると、コンパイルエラーになはずですnpm install -D @types/nodeで戻せるので気軽にどうぞ!)

「がんばらないTypeScript」の設定と実行

エンジニアHubに会員登録すると
続きをお読みいただけます(無料)。
登録のメリット
  • すべての過去記事を読める
  • 過去のウェビナー動画を
    視聴できる
  • 企業やエージェントから
    スカウトが届く