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

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

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

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

5.1 --init

--initオプションについて解説します。このオプションを使うと、TypeScriptでコードを書き始める際に必要なtsconfig.jsonの雛形を生成します。生成されたファイルはtscコマンドがコンパイルを行うときに読み込まれます。後述の--projectオプションも参照してみてください。

TypeScriptではプロジェクトのビルドに必要なコンパイルオプションや、コンパイル対象の指定などをtsconfig.jsonファイルにまとめていきます。このファイルはすべてのツールやIDE・エディタ間で共通に利用できる設定ファイルになるため、大変役立ちます。

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

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

{
  "compilerOptions": {
    // 様々なオプションの説明が出力されるが紙面の都合でカットです!
    "target": "es5",
    "module": "commonjs",
    "strict": true // 次の4オプションを一括で有効にする
    // "noImplicitAny": true,
    // "strictNullChecks": true,
    // "noImplicitThis": true,
    // "alwaysStrict": true,
  }
}

tsconfig.jsonに記述可能なプロパティは、おおむね次の5つです。

  • extends
  • compilerOptions
  • files
  • include
  • exclude

extendsには、相対パスまたは絶対パスで別のtsconfig.jsonを指定し、設定内容を引き継ぐことができます。拡張子の.jsonは省略可能です。

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

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

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

filesには、コンパイル対象にするファイルを列挙します。時期的に後述するincludeやexcludeよりも先に実装されたのですが、現状あまりにも面倒くさいので大抵使いません。

include、excludeはコンパイル対象とするファイルやフォルダを大まかに指定します。includeで調べるディレクトリやファイル、excludeで除外するディレクトリやファイルを指定し、この組み合わせで処理対象を限定します。ディレクトリを指定すると、そこに含まれるすべての.tsファイルと.tsxファイルが対象になります。簡単なワイルドカードも利用できます。

includeとexcludeをまったく指定しない場合、TypeScriptコンパイラは処理中のディレクトリやサブディレクトリ配下を調べ、すべての.tsファイルや.tsxファイルをコンパイルしようとします。これにはnode_modulesなども含まれてしまうため、大抵は不都合です。

includeとexcludeの利用例を見てみましょう(リスト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も利用できます。

通常、引数無しでtscコマンドを実行するとカレントディレクトリかそれより上にあるtsconfig.jsonを探して実行してくれます。その挙動だと困る場合にこのオプションを使います。オプションの値としてtsconfig.jsonがあるディレクトリか、tsconfig.jsonのパスを指定します。具体的にはtsc -p ./またはtsc -p ./tsconfig.jsonとします。

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

gulpやwebpackなどのタスクランナーやバンドラを使う場合も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 --strict

tsc --initで生成したときにデフォルトで有効になっているオプションです。このオプションは後述の--strictNullChecks--noImplicitAny--noImplicitThis--alwaysStrictの4オプションを一括で有効にします。

これがデフォルトで有効になっているということは、TypeScript開発チームは皆さんにガッツリ堅牢なコードを書いていってほしい、と思っているということですね。新規にプロジェクトを作成する場合などは必ず有効にして冒険の旅に出るようにしましょう。

5.5 --strictNullChecks

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

まずはオプションありの例です(リスト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.12 非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にできてしまいます。きっちりコードを書けば、オプション無しでも堅牢なアプリケーションを構築することは不可能ではありません。しかし、それはプログラマの不断の努力の上にしか成り立ちません。そんな苦労をするよりは、コンパイラにしっかりチェックしてもらえたほうがコードの堅牢さが確かなものになりますね。

もちろん本書も--strictNullChecksオプションを常に有効にしている前提で書いています。

5.6 --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.7 --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.8 --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.9 --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.10 --alwaysStrict

"use strict";ディレクティブを常時出力するオプションです。ECMAScriptのモジュールを使ったときのように言語仕様上必ず"use strict";を出してくれる場合もありました。このオプションを使うとそれが常時適用される状態になります。今の時代、strictモードをあえて使わないという選択肢はないはずですのでこれも常時使うことをお勧めするオプションです。

5.11 --target

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

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

  • es3
  • es5
  • es6 / es2015
  • es2016
  • es2017
  • esnext

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

GeneratorやIteratorをes5のコードにダウンパイルしたい場合は--downlevelIterationを併せて指定します。

5.12 --module、--moduleResolution

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

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

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

これも明確な事情がない限り、今のところはcommonjsでよいでしょう。前述の--targetと自由に組み合わせることができるため、--target es5としつつ--module esnextとすることもできます。

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

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

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

基本的にはnode一択でよいでしょう。

5.13 --lib

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

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

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

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

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

5.14 --types

--typesオプションについて解説します。TypeScriptのコンパイルを行う際、参照するべき型定義ファイルを明示的に指定します。

通常、ソースコード中でimportしたモジュールのための型定義ファイルはルールにしたがって発見されるため、この設定は不要です。しかしながら、Node.jsやテスティングフレームワークのmochaなど、実行環境に初めからセットアップされているものはimportの機会がありません。このため、何らかの方法でコンパイラに環境の情報を伝える必要があります。そこで使うのが--typesオプションです(リスト5.11)。

リスト5.11: types指定でnodeとmochaを参照する

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "types": [
      "node",
      "mocha"
    ]
  }
}

ここで利用する型定義ファイルは第6章「JavaScriptの資産と@types」で紹介する@typesからnpmで引っ張ってくるのがお手軽です。npm install --save-dev @types/node @types/mochaという具合ですね。

5.15 --forceConsistentCasingInFileNames

--forceConsistentCasingInFileNamesオプションについて解説します。このオプションを有効にすると、ファイル名の参照について大文字小文字の食い違いがあるとエラーにします。

macOSのような非ケースセンシティブな環境と、Linuxのようなケースセンシティブな環境が混在しているとき、macOSではエラーにならないけどLinuxではエラーになる…のようなシチュエーションを防止してくれます。チーム内でmacOSに統一されていても、外部の人やCIサーバなどはLinuxを使っている場合などはかなり多いため、とりあえず有効にしてしまってよいでしょう。

5.16 --noEmitOnError、--noEmit

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

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

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

5.17 --importHelpersと--noEmitHelpers

TypeScriptで--target es5などでダウンパイルした場合、ヘルパ関数が自動生成されます。たとえば、クラスの継承を行ったときは__extends関数が生成されますね(リスト5.12)。

リスト5.12: 生成される__extends関数

var __extends = (this && this.__extends) || (function () {
  var extendStatics = Object.setPrototypeOf ||
    ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
    function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
  return function (d, b) {
    extendStatics(d, b);
    function __() { this.constructor = d; }
    d.prototype = b === null ?
        Object.create(b) : (__.prototype = b.prototype, new __());
  };
})();

--importHelpers--noEmitHelpersを併用すると、ヘルパ関数がファイル毎に生成されるのを抑制し、1つにまとめることができます。

--importHelpersを利用する場合、npm install --save tslibが必要になります。ヘルパ関数を生成する代わりにtslibパッケージ内のヘルパ関数を使うようにする構造です(リスト5.13)。

リスト5.13: tslibが利用される例

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var Base = (function () {
    function Base() {
    }
    return Base;
}());
exports.Base = Base;
var Inherit = (function (_super) {
    tslib_1.__extends(Inherit, _super);
    function Inherit() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    return Inherit;
}(Base));
exports.Inherit = Inherit;

--noEmitHelpersを利用する場合、単純にヘルパ関数が出力されなくなります。つまり、ヘルパ関数がグローバルな空間に定義された状態を作り、生成されたJSコードから参照できるようにしてやる必要があります(リスト5.14)。

リスト5.14: 唐突に参照されるヘルパ関数

"use strict";
var Base = (function () {
    function Base() {
    }
    return Base;
}());
var Inherit = (function (_super) {
    __extends(Inherit, _super);
    function Inherit() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    return Inherit;
}(Base));

現代的にはJSコードは1つにbundleし、gzipで圧縮して転送されるユースケースが多いでしょう。つまり、tslibを使ってもさほど有利にはならないかもしれません。TypeScriptが生成するデフォルトのヘルパ関数から実装を差し替えたい場合に使えるかもしれませんね。そして、一見便利そうなヘルパライブラリなのですがtscの実装は若干バギー(というか不親切)です。

tslibはモジュールの形でimportされるため、moduleをcommonjsやesnextなどに指定している場合でもimport句またはexport句がない場合、tslibの参照が行われません。--importHelpers--noEmitHelpersを併用しているとtslibの参照が行われない上にヘルパ関数の生成も行われなくなります。両オプションを利用する場合、安全側に倒すため、どちらか片方のオプションだけを利用するほうが安全でしょう。

なお、TypeScript 2.3.2以前でtslibを使う場合、tslibのバージョンは1.6.1を使います。

5.18 pluginsの設定

pluginsも仕組みとして面白いので概要だけ言及しておきます。第8章「ツールとしてのTypeScript」でも少し扱います。

pluginsはその名のとおりプラグインなのですが、現時点では効果を及ぼすことができる対象はエディタやIDE上のみです。コンパイル時の動作には影響を及ぼすことができません。そのため、tscのオプションとしてはpluginsは存在せず、tsconfig.jsonの設定項目としてのみ存在します。

このオプションを設定しておくと、エディタでTypeScriptの入力補完やコンパイルエラーの表示動作を拡張できます。現時点で実際に使える(と思われる)npmパッケージを次に挙げます*4

  • @angular/language-service
  • ts-graphql-plugin
  • tslint-language-service*5

また、筆者の作った役にも立たないプラグインを@vvakame/typescript-plugin-example*6として公開しています。これは入力補完候補の説明文とかクイックインフォの説明文の末尾に猫の絵文字などを出すだけのものです。設定例と動作イメージの紹介にちょうどいいでの確認してみます。tsconfig.jsonの内容はリスト5.15で、動作例は図5.1です。

リスト5.15: pluginsの設定例

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "plugins": [
      {
        // name は全プラグイン共通で必須
        "name": "@vvakame/typescript-plugin-example",
        // プラグイン固有の設定を行う場合がある
        "verbose": true,
        "goody": "(`_´)"
      }
    ]
  }
}
tsconfig.jsonの設定内容を反映して顔文字が出ている

図5.1: tsconfig.jsonの設定内容を反映して顔文字が出ている

簡単にエディタの機能を拡張できるので楽しいですね。まだまだこの仕組を使っているパッケージは少ないので、よいアイディアがあればどんどんやってみましょう。