第5章 オプションを知り己のコードを知れば百戦危うからず

本章ではtscのコマンドラインオプションについて解説していきます。すべてを網羅することはしませんが、いくつかの重要なオプションについては知ることができるでしょう。

本章記載のオプションはtsconfig.jsonのcompilerOptionsに記載可能なプロパティ名と同一です。tsconfig.jsonでは短縮形(-d-p)は利用できないことに注意してください。

ここに記載されていないオプションで知りたいものがあれば本書のIssue*1にお寄せください。

5.1 --init

--initオプションについて解説します。このオプションを使うと、TypeScriptでコードを始める時に必要なtsconfig.jsonの雛形を生成します。生成されたファイルは後述の--projectオプションと組み合わせて使います。TypeScriptではプロジェクトのビルドに必要なコンパイルオプションや、コンパイル対象の指定などをtsconfig.jsonファイルにまとめていきます。このファイルはすべてのツールやIDE・エディタ間で共通に利用できる設定ファイルになるため、大変役立ちます。

まずはtsc --initコマンドで生成されるtsconfig.jsonを見てみます(リスト5.1)。

リスト5.1: 生成されたtsconfig.json

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es5",
        "noImplicitAny": false,
        "sourceMap": false
    },
    "exclude": [
        "node_modules"
    ]
}

tsconfig.jsonに記述可能なプロパティは概ね次の4つです。

  • compilerOptions
  • files
  • include
  • exclude

compilerOptionsには、コンパイル時に利用するオプションを指定します。コンパイルオプションの名前とcompilerOptionsに記載可能なプロパティ名は一致しています。たとえばリスト5.1はtsc --module commonjs --target es5という意味になります。--noImplicitAny--sourceMapの値はfalseなのでオプションとして指定していない状態を指します。

tsconfig.jsonで利用可能なcompilerOptionsについては、本章を読むか公式ハンドブックの解説*2JSON Schemaの定義*3を参照してください。

残る3つはコンパイル対象にするファイルを指定するためのプロパティです。3つすべてに共通の挙動として、コンパイル対象に明示的に含めない場合でもTypeScriptコンパイラが自動的に依存関係を解決し必要なファイルを対象に含めてくれる場合があります。この機能は歓迎すべき機能で、余計な設定の手間を減らしてくれます。

filesには、コンパイル対象にするファイルを1つ1つ列挙します。あまりにも面倒くさいため、tsconfig-cli*4などのツールを利用する必要がありました。これの反省を踏まえ、次に説明するinclude、excludeが導入されました。

include、excludeはコンパイル対象とするファイルやフォルダを大まかに指定します。includeとexcludeを全く指定しない場合、TypeScriptコンパイラは処理中のディレクトリやサブディレクトリ配下を調べ、すべての.tsファイルや.tsxファイルをコンパイルしようとします。

そこでincludeで調べるディレクトリやファイルを、excludeで除外するディレクトリやファイルを指定し処理対象を限定します。include、excludeの2つは組み合わせて使うのが一般的です。ディレクトリを指定すると、そこに含まれるすべての.tsファイルと.tsxファイルが対象になります。簡単なワイルドカードも利用できます。例を見てみましょう(リスト5.2)。

リスト5.2: 使えるワイルドカードの例

{
    "compilerOptions": {
        "listFiles": true, // コンパイルの処理対象を表示する
        "noEmit": true     // コンパイル結果の.jsファイルなどを出力しない
    },
    "include": [
        /// ディレクトリのワイルドカード
        // /**/ で全てのサブフォルダ
        "libA/**/*",
        // /*/ で直下のサブフォルダ
        "libB/*/*",

        /// 文字のワイルドカード
        // * で0文字以上にマッチする
        "libC/*.ts",
        // ? で1文字にマッチする
        "libD/?b.ts"
    ],
    "exclude": [
        "node_modules",
        // 除外でも同じようにワイルドカードが使える
        "libD/b*.ts"
    ]
}

例で示したtsconfig.jsonを利用してみます。リスト5.3はプロジェクト内部に存在するts関連ファイルと、ファイルが処理されるかされないかを突き合わせたものです。

リスト5.3: ファイル一覧とマッチの結果

libA/a/index.ts    # 対象になる
libA/a/b/index.ts  # 対象になる
libB/index.ts      # 対象にならない
libB/a/index.ts    # 対象になる
libC/index.ts      # 対象になる
libC/index.tsx     # 対象にならない
libD/ab.ts         # 対象になる
libD/ac.ts         # 対象にならない
libD/bb.ts         # 対象にならない(exclude)

なかなか素直な結果です。単にワイルドカードであって、正規表現で記述できるわけではない点に注意しましょう。

5.2 --project

--projectオプションについて解説します。短縮記法の-pも利用できます。

このオプションでプロジェクトのコンパイルを行います。オプションの値としてtsconfig.jsonがあるディレクトリか、tsconfig.jsonのパスを指定します。具体的にはtsc -p ./またはtsc -p ./tsconfig.jsonとします。

tsconfig.jsonではない名前のファイルを使って、プロジェクト内に複数のビルド構成を作ることもできます。しかし、その場合IDE・エディタ側が設定をうまくハンドリングしてくれない場合が多いため、基本的には努力して1プロジェクトにつき1つのtsconfig.jsonとなるようにしましょう。

gulpやgruntなどのタスクランナーを使う場合でもtsconfig.jsonを用意し--projectオプションのみでコンパイルを通せる環境を維持するのがよいでしょう。

5.3 --noImplicitAny

TypeScriptコンパイラの最重要オプション、--noImplicitAnyについて解説します。このオプションは"暗黙的なanyを禁ずる"の名が表すとおり、型推論の結果、暗黙的に変数の型がanyになった場合、エラーとしてくれます。

リスト5.4のようなメソッドの返り値の型を書き忘れた!という脇の甘いコードをコンパイルしてみます。

リスト5.4: メソッドの返り値を書き忘れた!

declare class Sample {
  // 返り値の型を指定し忘れている!
  // error TS7010: 'method', which lacks return-type annotation,
  //               implicitly has an 'any' return type.
  method();
}

// 仮引数wordに型注釈がない
// error TS7006: Parameter 'word' implicitly has an 'any' type.
function hi(word) {
  word = word || "TypeScript";
  console.log(`Hello, ${word}`);
}

export { }
$ tsc --noImplicitAny definition.d.ts
definition.d.ts(3,5): error TS7010: 'method', which lacks return-type
    annotation, implicitly has an 'any' return type.

返り値の型を書いていなかったり、関数の仮引数の型が指定されていなかったりしたため暗黙的にanyになってしまいました。このようなときに、それはダメだ!とコンパイラが教えてくれます。anyが紛れ込んで型チェックが意味を成さなくなるとTypeScriptの意義が薄れてしまいます。型定義ファイルを書くときも、通常の開発時も、常に--noImplicitAnyを使うようにしましょう。

5.4 --strictNullChecks

--strictNullChecksオプションについて解説します。このオプションはnullやundefinedの扱いについてより厳格にし、変数の中身についての曖昧さを積極的に排除するよう振る舞います。nullやundefinedを許容したい場合、union typesや省略可能引数を使って明示的にnullやundefinedである可能性を示さなければなりません。

本書は--strictNullChecksオプションを常に有効にしている前提で書いています。有効にしている時の挙動は本書のサンプルすべてが該当しますので、この節ではこのオプションを使わないときの挙動について確認します。

まずはオプションありの例です(リスト5.5)。

リスト5.5: 危険なコードがいち早く発見される

// --strictNullChecks無しとの比較
// 無しの場合に等しい表現は…
let objA: Date;
// コレです
let objB: Date | null | undefined;

objA = new Date();
// Date単体の型注釈の場合、エラーとなる
// error TS2322: Type 'null' is not assignable to type 'Date'.
// objA = null;

// …しかし、一回anyを経由すればごまかせてしまう 他のサンプルコードでもたまにやってます
objA = null as any;

// objB は null も undefined も許容するため、ゆるゆる
objB = new Date();
objB = null;
objB = void 0; // undefined

// 処理フロー的にundefinedが確定しているのでエラーとなる
// error TS2532: Object is possibly 'undefined'.
// error TS2339: Property 'getTime' does not exist on type 'never'.
// objB.getTime();

// 非null指定演算子(!)で無理やりコンパイルを通すこともできる
objB!.getTime();

export { }

nullやundefinedに対するアクセスが多くの場合未然に防がれ、"コンパイルが通ればもう安全"であるコードが書きやすいことがわかります。

非null指定演算子(!)については第4章「アドバンスド型戦略」の「4.11 非null指定演算子(Non-null Assertion Operator)」で触れました。

さて次はオプションなしの例です(リスト5.6)。

リスト5.6: 実行時にエラーになるかも

// --strictNullChecks無しだと大変ゆるい
let obj: Date;
// 全部OK!
obj = new Date();
obj = null;
obj = void 0; // undefined

// 処理フロー的にはundefinedだけど怒られない
obj.getTime();

export { }

ゆるゆるですね。変数の中身を容易にnullやundefinedにできてしまいます。きっちりコードを書けば、オプション無しでも堅牢なアプリケーションを構築することは不可能ではありません。しかし、それはプログラマの不断の努力の上にしか成り立ちません。そんな苦労をするよりは、コンパイラにしっかりチェックしてもらえたほうがコードの堅牢さが確かなものになりますね。

5.5 --noUnusedLocals

--noUnusedLocalsオプションについて解説します。その名のとおり、使っていないローカル変数があったらエラーにしてくれます。本書のサンプルコードでも有効になっているため、エラー消しのために無意味にexportしている箇所がありました。

例を見てみます(リスト5.7)。

リスト5.7: 未使用変数はちゃんと消そう

// importした後、一回も使わないのはエラー
// error TS6133: 'readFile' is declared but never used.
import { readFile } from "fs";

// 1回も参照されていないとエラーになる
// error TS6133: 'objA' is declared but never used.
let objA = {};

// どこかで参照されていればOK
let objB = {};
export { objB }

// exportしていればどこかで使われるかもしれないからOK
export let objC = {};

未使用の変数があるとエラーになります。まるでGo言語のようですね。エラーを削っていくと、import文自体を削減できるパターンもあるでしょう。コードをきれいに保とう!

5.6 --noUnusedParameters

--noUnusedParametersオプションについて解説します。関数やメソッドの引数に使っていないものがあるとエラーにしてくれます。エラーにせず残しておきたい場合、変数名の頭に_(アンダースコア)をつけることでエラーを抑制できます。

例を見てみます(リスト5.8)。

リスト5.8: 使っていない仮引数はできれば削除したい

// 仮引数 b は利用されていないのでエラー _c はプリフィクス_なのでエラーにならない
// error TS6133: 'b' is declared but never used.
export function foo(a: string, b: number, _c: boolean) {
  console.log(a);
}

export class Sample {
  // 仮引数 a は利用されていないのでエラー
  // error TS6133: 'a' is declared but never used.
  method(a: string) {
  }
}

未使用の仮引数があるとエラーになります。関数の引数の数や型を後から変更するのはめんどくさいので、なるべく早めに検出し修正してしまいたいものです。

5.7 --noImplicitReturns

--noImplicitReturnsオプションについて解説します。関数やメソッドの返り値について、returnで値を返す場合とreturnしない場合、エラーになります。

例を見てみます(リスト5.9)。

リスト5.9: 暗黙のreturnを禁じる

// returnがない(暗黙的にundefinedが返る)パターンを検出してくれる
// error TS7030: Not all code paths return a value.
function a(v: number) {
  if (v < 0) {
    return "negative";
  } else if (0 < v) {
    return "positive";
  }

  // return がない!
}

function b() {
  // そもそも常にreturnがないならOK
}

export { }

プログラミングのスタイルとして、elseの漏れや値の返し忘れがあるコードはミスである可能性が高いです。そういったコードを書くとエラーになるのは便利ですね。

5.8 --noImplicitThis

--noImplicitThisオプションについておさらいします。第4章「アドバンスド型戦略」の「関数のthisの型の指定」で述べたとおり、このオプションを利用すると、thisの型指定がない関数内でthisへアクセスするとエラーになります。

例を見てみます(リスト5.10)。

リスト5.10: 型指定無しのthisの利用を禁じる

// 関数内部でのthisの型を偽の第一引数で指定
function testA(this: string) {
  console.log(this.toUpperCase());
}
testA.bind("TypeScript")();

function testB() {
  // --noImplicitThisオプション利用時、関数内でthisにアクセスすると怒られる
  // error TS2683: 'this' implicitly has type 'any'
  //   because it does not have a type annotation.
  console.log(this.toUpperCase());
}

export { testB }

5.9 --target

--targetオプションについて解説します。短縮記法で-tも利用できます。TypeScriptのコンパイルを行う際、ECMAScript 3(超古い!)、ECMAScript 5(古い!)、ECMAScript 2015(最近)のどのバージョンをターゲットとするかを指定します。これは、"TypeScriptコードをどのバージョンで書くか"ではなく、"書いたTypeScriptをどのバージョンに変換するか"の指定です。TypeScriptでは基本的に最新の記法で書き、ダウンパイル(古い書き方へ変換)します。

利用可能なオプションの値は次のとおりです。

  • es3
  • es5
  • es6 / es2015

基本的に、IE11などの少し古いブラウザのサポートを切らないのであればes5を選択すればよいでしょう。es3の利用はもはやお勧めしません。

一部、Generatorやasync/awaitなどの記法はダウンパイルできません。これらは2.1.0でサポートされる予定なので、延期されないようにみんなで祈りましょう。

5.10 --module、--moduleResolution

--moduleオプションについて解説します。短縮記法で-mも利用できます。TypeScriptはモジュールをコンパイルする際に、どの形式に変換するかを選ぶことができます。

利用可能なオプションの値は次のとおりです。

  • none
  • commonjs
  • system(SystemJS)
  • umd
  • es6 / es2015

これも明確な事情がない限り、今のところはcommonjsでよいでしょう。

--moduleResolutionオプションについて少し触れておきます。モジュールの名前解決の方法について指定できます。

利用可能なオプションの値は次のとおりです。

  • node
  • classic(TypeScript 1.6以前の形式)

基本としてnode一択でよいでしょう。

前述の--targetと自由に組み合わせることができるため、--target es5としつつ--module es6とすることもできます。この組み合わせが可能になったのはTypeScript 2.0.0からなので、Rollup.js*5との組み合わせての運用はまだ未知数です。TypeScript+Rollup.jsをプロジェクトに導入してみてブログ記事などにまとめてみると話題になるかもしれません。お待ちしています!

5.11 --lib

--libオプションについて解説します。TypeScriptのコンパイルを行う際、標準の型定義として何を使うかを個別に指定できます。たとえ、--target es5としてダウンパイルする場合でも、利用する型定義はes2015にできるのです。最近はPromiseを使ったAPIは珍しくないですし、かつIE11でも動かしたい場合というのはザラにあります。

利用可能なオプションの値は次のとおりです。複数指定したい場合、コマンドラインオプションの場合は,で区切ります。tsconfig.jsonの場合は素直に配列にしましょう。

  • dom
  • webworker
  • es5
  • es6 / es2015
  • es2015.core
  • es2015.collection
  • es2015.iterable
  • es2015.promise
  • es2015.proxy
  • es2015.reflect
  • es2015.generator
  • es2015.symbol
  • es2015.symbol.wellknown
  • es2016
  • es2016.array.include
  • es2017
  • es2017.object
  • es2017.sharedmemory
  • scripthost

自分のプロジェクトの用途を考え、適切なものを選びましょう。たとえばNode.jsなプロジェクトであればHTMLElementなどは不要でしょうからdomはいらないです。多くのプロジェクトではes2017か、+domの指定があれば十分でしょう。

es2017を利用する場合はes2017の型定義にes2016の参照が含まれます。どの標準型定義ファイルが何を参照しているかが気になる場合は直接型定義ファイルを見るか、--listFilesオプションをつけてコンパイルしてみたりするとよいでしょう。

5.12 --forceConsistentCasingInFileNames

--forceConsistentCasingInFileNamesオプションについて解説します。このオプションを有効にすると、ファイル名の参照について大文字小文字の食い違いがあるとエラーにします。macOSのような非ケースセンシティブな環境と、Linuxのようなケースセンシティブな環境が混在しているとき、macOSではエラーにならないけどLinuxではエラーになる…のようなシチュエーションを防止してくれます。チーム内でmacOSに統一されていても、外部の人やCIサーバなどはLinuxを使っている場合などはかなり多いため、とりあえず有効にしてしまってよいでしょう。

5.13 --noEmitOnError、--noEmit

--noEmitOnErrorオプションと--noEmitオプションについて解説します。このオプションは成果物である.jsファイル、.js.mapファイル、.d.tsファイルを生成するか否かを制御します。

--noEmitOnErrorはコンパイルが成功した時のみファイルを生成します。これはgruntやgulpなどのタスクランナーを利用する際の「コンパイル成功したつもりだったけど失敗してた。後続のタスクが続いてしまい失敗を見逃した」というパターンに有効です。前回の生成物を削除してからコンパイルするようにすることで.jsファイルが必要なステップで処理全体が確実に落ちるようにできます。「そんなクソみたいなタスク作らないよ!」と思うかもしれないですが、これが案外やりがちなのです。プロジェクトの健康を保つためにも、--noEmitOnErrorオプションは常に有効でよいでしょう。

--noEmitオプションはコンパイルが成功しようが失敗しようが、常に成果物を何も生成しません。tsc -p ./でファイルを生成するのとは違う手順でのみビルドを行う場合、例えばwebpackでts-loaderを使っているプロジェクトなどで有効です。tsc -p ./ --noEmitとすることでTypeScriptのコンパイルエラーのみをチェックできます。これはビルドタスク全体を走らせるよりも手短で、作業ディレクトリに不要なファイルを撒き散らすこともありません。