Help us understand the problem. What is going on with this article?

tsconfig.jsonの全オプションを理解する(随時追加中)

概要

社内勉強会の資料。
TypeScriptのtsconfig.jsonにはオプションが色々とあるので、それらの意味や用途を理解する目的です。

tscのバージョンは3.7.2を使用します。
VSCodeのバージョンは1.40.1を使用します。

公式のドキュメント:https://www.typescriptlang.org/docs/handbook/tsconfig-json.html

公式ドキュメントの和訳:http://js.studio-kingdom.com/typescript/project_configuration/tsconfig_json

また、各項目の詳しい説明はdetailsタグを使ってデフォルトで非表示にしています(全部デフォルトで表示するとめっちゃ長いので)。「詳しく」の部分をクリックすると展開されます。

2020年5月24日追記
公式のtsconfig.jsonのドキュメントがめっちゃ綺麗になったので、まずはこっち見た方が良いです!
https://www.typescriptlang.org/tsconfig

準備

typescriptをインストールして、tsc --initする。

$ mkdir tsconfig; cd tsconfig
$ npm init
$ npm install --save-dev typescript
$ npx tsc -v
Version 3.7.2
$ npx tsc --init
message TS6071: Successfully created a tsconfig.json file.
$ ls -1
node_modules
package-lock.json
package.json
tsconfig.json
$ cat tsconfig.json

init直後のtsconfig.jsonを表示(長い)
{
  "compilerOptions": {
    /* Basic Options */
    // "incremental": true,                   /* Enable incremental compilation */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    // "lib": [],                             /* Specify library files to be included in the compilation. */
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    // "outDir": "./",                        /* Redirect output structure to the directory. */
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    // "noUnusedLocals": true,                /* Report errors on unused locals. */
    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
    // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
    // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */

    /* Advanced Options */
    "forceConsistentCasingInFileNames": true  /* Disallow inconsistently-cased references to the same file. */
  }
}

ここにあらかたの説明は書かれている。が、正直この説明だけでは何のことやらわからないので、一つずつ見ていく。

ちなみに、VSCodeを使っている場合は、使用できるオプション名やその値は補完が効くので便利。

第一階層

compilerOptions

"compilerOptions": {}

コンパイルする際のオプション。基本的にここにオプションを書いていく。

files

"files": [
  "core.ts",
  "sys.ts",
  "types.ts",
]

コンパイルするファイルを直接指定する。
こちらはincludeexcludeと違ってワイルドカードを使用できない。
拡張子は省略可能。省略した場合、.tsファイルが優先して対象になる(hoge.tshoge.tsxがある際に"hoge"を指定すると.tsファイルだけがコンパイルされる)。

include

コンパイルする対象ファイルを記述する。
ワイルドカード(*,?,**/)が使える。

  • * 0個以上の文字に一致します(ディレクトリ区切り文字を除く)
  • ? 任意の1文字に一致します(ディレクトリ区切り文字を除く)
  • **/ 任意のサブディレクトリに再帰的に一致します
"include": [
  "src/**/*"
]

上記の場合は、src配下のファイル全て(ディレクトリのネストが深くなっても再帰的に全て)がコンパイル対象。

拡張子を指定しない(*のみ)、もしくは拡張子にアスタリスクを使用する(hoge.*)場合、デフォルトでは下記の拡張子のファイルのみが対象になる。

  • .ts
  • .tsx
  • .d.ts

ただし、compilerOptions.allowJstrueの場合は、下記の拡張子も含む。

  • .js
  • .jsx

filesincludeも指定しない場合、tsconfig.jsonが置かれているディレクトリ配下の全てのTypeScriptファイル(拡張子が.ts.d.ts.tsxであるファイル)のうち、excludeに含まれるファイル以外がコンパイル対象になる。

exclude

コンパイルする対象から外すファイルを記述する。
includeと同じワイルドカードが使える。

excludeは指定しない場合デフォルトで以下の値を含む。

  • node_modules
  • bower_components
  • jspm_packages
  • outDirオプションで指定しているディレクトリ配下のファイル

逆に言うと、tsconfig.jsonexcludeオプションが指定されている場合は、outDirの中身も対象になってしまう。

詳しく
tsconfig.json
"outDir": "./dist"

"exclude": ["node_modules"]
$ ls -1 dist
piyo.ts

となっているとしましょう。

そのまま tsc コマンドを叩くと、この場合excludeが明示的に指定されているため、outDirの中身はexcludeの対象になりません。
そのため、アウトプットはこうなります。

dist/
├── dist
│   └── piyo.js
└── piyo.ts

ややこしいですが、一つ目のdistディレクトリはoutDirで指定したディレクトリです。つまり、distディレクトリもコンパイル対象になっているため、dist/piyo.tsをコンパイルしたアウトプットであるdist/piyo.jsが、outDirで指定されているdistディレクトリに入ってしまっています。

まあ、outDirに指定しているディレクトリにコンパイル前のファイルを入れることは基本無いと思います。

filesとincludeとexcludeの適用順番

簡単に言うとfilesが最強、include < exclude。

includeで指定されたファイルはexcludeで除外されるが、filesに指定したファイルはexcludeに関係なく問答無用でコンパイル対象になる。

また、このルールによってコンパイル対象になったファイルが参照するファイルもコンパイル対象に含まれる。

extends

tsconfig.jsonは複数のファイルに分割することができ、このextendでファイルパスを指定することで、そのtsconfig.jsonが継承するtsconfig.jsonファイルを指定できる。子は親の設定を上書きする。

"extends": "./configs/base"

テストだけ設定を変えるなど、環境ごとの出力の違いに対応する場面などで使う。

compileOnSave

この値をtrueにすると、tsconfig.jsonを保存した際にコンパイルが走るようになる。

This feature is currently supported in Visual Studio 2015 with TypeScript 1.8.4 and above, and atom-typescript plugin.

とのことなので、これを満たしていれば使える。
役に立つんだろうか、これ...

references

"references": [
  { "path": "../src" }
]

Project Referencesを有効にする際に使用する。
pathにはこのtsconfig.jsonが存在するディレクトリから参照する別のtsconfig.jsonが存在するディレクトリパス、またはその別のtsconfig.jsonのパスを指定する。
後述のcompilerOptions.compositeと組み合わせて使う。

Project Referencesに関しては説明に時間がかかるので一旦省略。
上記の公式ドキュメント読むとわかります。

compilerOptions

target

"target": "es5"

Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'.

どのバージョンでjsを出力するか。
つまりデフォルトのES3であれば、コンパイルの結果出力されるjsはES3に準拠しているjsコードということになる。

詳しく
tsconfig.json
"target": "es3"
index.ts
const func = async () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("5秒経ちました");
    }, 5000);
  });
};

だとしましょう。
これをコンパイルした結果はこちら。

コンパイル結果
index.js
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var func = function () { return __awaiter(void 0, void 0, void 0, function () {
    return __generator(this, function (_a) {
        return [2 /*return*/, new Promise(function (resolve) {
                setTimeout(function () {
                    resolve("5秒経ちました");
                }, 5000);
            })];
    });
}); };

そして次はtarget"target": "es2019"にしてコンパイルした結果がこちら。

コンパイル結果
index.js
"use strict";
const func = async () => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve("5秒経ちました");
        }, 5000);
    });
};

これはつまり、es3に準拠させようとするとasyncやPromiseなんて存在しないので、レガシーなjsコードでPromiseやasyncを再現するPolyfilが手前で生成されていて、それを使用して元のコードを再現している、そしてes2019に準拠させる場合は、asyncやPromiseはすでに標準で組み込まれているので、そのまま出力されるということです。

ここはTypeScriptのコンパイルというよりはBable的な役割ですね。

lib

"lib": ["dom", "es2019"]

Specify library files to be included in the compilation.

コンパイルする際に使用する組み込みライブラリを指定する。

基本的にはtargetで指定しているjsのバージョンに含まれているものは暗黙的に指定される。
ただし、targetに指定しているjsのバージョンには含まれていない組み込みライブラリを使用する場合は、明示的な指定が必要。

詳しく

例えば、tsconfig.jsontargetを指定しない場合、デフォルトではES3になるため、ES3に含まれていない組み込みライブラリはエラーになる。

index.ts
const func = async () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("5秒経ちました");
    }, 5000);
  });
};
index.ts:2:14 - error TS2585: 'Promise' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the `lib` compiler option to es2015 or later.

2   return new Promise(resolve => {
               ~~~~~~~

(asyncは組み込みライブラリではなく記法なのでこの場合は関係なし)

こういう場合は、lib

tsconfig.json
"lib": ["dom", "es2015"]

と指定することでエラーを回避できる。

ちなみに、明示的に指定すると暗黙的に指定されていたものが指定されなくなるので、必要なものは全て列挙する必要がある。
この場合、setTimeoutを使っていて、こういったブラウザで提供されるライブラリを使用するには"dom"を指定する必要がある。
(つまりlibを指定していない場合はdomも暗黙的に指定されている)

module

module: "esnext"

Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.

デフォルト値は下記。

target === "ES3" or "ES5" ? "CommonJS" : "ES6"

出力するjsのモジュールの仕組みとして何を使用するかを指定する。

詳しく
index.ts
export const func = async () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("5秒経ちました");
    }, 5000);
  });
};
tsconfig.json
"module": "commonjs"

の場合、コンパイル結果は

index.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.func = async () => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve("5秒経ちました");
        }, 5000);
    });
};
tsconfig.json
"module": "es2015"

の場合、コンパイル結果は

index.js
export const func = async () => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve("5秒経ちました");
        }, 5000);
    });
};

outDir

"outDir": "./dist"

Redirect output structure to the directory.

何も指定しない場合、コンパイルされたjsはコンパイルしたtsファイルと同じディレクトリに作成される。
このオプションでディレクトリを指定した場合、tsファイルのディレクトリ構成をそのままに保ちつつ、指定したディレクトリにjsファイルを作成する。

詳しく
.
├── index.ts
├── package-lock.json
├── package.json
├── src
│   ├── hoge.ts
│   └── piyo
│       ├── foo
│       │   └── foo.ts
│       └── piyo.ts
└── tsconfig.json

こういうディレクトリ構成だった場合、何も指定しない場合は

.
├── index.js
├── index.ts
├── package-lock.json
├── package.json
├── src
│   ├── hoge.js
│   ├── hoge.ts
│   └── piyo
│       ├── foo
│       │   ├── foo.js
│       │   └── foo.ts
│       ├── piyo.js
│       └── piyo.ts
└── tsconfig.json

こうなる。

"outDir": "./dist"を指定すると

.
├── dist
│   ├── index.js
│   └── src
│       ├── hoge.js
│       └── piyo
│           ├── foo
│           │   └── foo.js
│           └── piyo.js
├── index.ts
├── package-lock.json
├── package.json
├── src
│   ├── hoge.ts
│   └── piyo
│       ├── foo
│       │   └── foo.ts
│       └── piyo.ts
└── tsconfig.json

こうなる。

ディレクトリ構成を保ちつつ指定したdistディレクトリにjsファイルが作成されている。

allowJs

"allowJs": true

Allow javascript files to be compiled.

これをtrueにしておくと、.js.jsxもコンパイル対象に含まれるようになる。
部分的にjsで書いている場合などにtrueにする。
tsでは無いので型チェックなどは行われないが、tschsjsのトランスパイル(ここではロジックの変更なしで記法を指定バージョンに準拠させる変換の意)なども行うので、その対象となる。

こちらの記事がわかりやすいです!
TypeScriptを徐々に導入する - Qiita

checkJs

"checkJs": true

Report errors in .js files.

allowJsの上記の記事のコメントで会話されているように、JSDocを使うことでjsファイルの型チェックを行うオプションです。
Type Checking JavaScript Files - github.com/Microsoft/TypeScript

tsで書き直すことは出来ないけど、型チェックの恩恵を受けたいみたいな場合に、JSDocの追加だけなら許容できる(コメントの追加であってコードの変更じゃないから)みたいな場面があるんでしょうか。

jsx

"jsx": "preserve"

Specify JSX code generation: 'preserve', 'react-native', or 'react'.

tsxファイルをjsxやjsにコンパイルする際の出力の形式を指定する。

詳しく

まずはpreserve

tsconfig.json
"jsx": "preserve"
component.tsx
declare namespace JSX {
  interface IntrinsicElements {
    section: any;
    h1: any;
    p: any;
  }
}

const Component = () => (
  <section>
    <h1>Component</h1>
    <p>Contents</p>
  </section>
);

(jsxの型についてはこちらの記事がわかりやすいです!TypeScriptの型におけるJSXサポートが100%分かる記事 - Qiita

の場合は

component.jsx
"use strict";
var Component = function () { return (<section>
    <h1>Component</h1>
    <p>Contents</p>
  </section>); };

こうなる。preserveはjsxをjsxのまま保持するので、拡張子は.jsxになる。
jsxのコンパイルをtscではなくBabelなど他に任せたい場合に使用する。

Reactのコードにコンパイルしたい場合は下記のように"react"を指定する。

tsconfig.json
"jsx": "react"

とりあえずコンパイルだけなので@types/reactだけインストール。

$ npm install --save-dev @types/react
component.ts
const Component = () => (
  <section>
    <h1>Component</h1>
    <p>Contents</p>
  </section>
);

@types/reactがインストールされていればjsxの型についてこちらが書く必要は無いのでこれだけ:arrow_up:

component.js
"use strict";
var Component = function () { return (React.createElement("section", null,
    React.createElement("h1", null, "Component"),
    React.createElement("p", null, "Contents"))); };

結果はこれ:arrow_up:。jsxの記法が完全に消え、拡張子も.jsになる。

tsconfig.json
"jsx": "react-native"

react-nativeは使う予定が無いので一旦パス。

declaration

"declaration": true

Generates corresponding '.d.ts' file.

これをtrueにすると、コンパイルしたtsファイルの中でexportしているもの全ての型定義ファイルをファイルごとに作成する。
exportが一つもなくてもファイル自体は作成される。

詳しく
tsconfig.json
"declaration": true
.
├── index.ts
├── package-lock.json
├── package.json
├── src
│   ├── hoge.ts
│   └── piyo
│       ├── foo
│       │   └── foo.ts
│       └── piyo.ts
└── tsconfig.json

こういうディレクトリ構成だとして、index.tsは、

index.ts
console.log("hoge");

export const func = async () => {
  // Promiseにジェネリクスで型を渡さないと`resolve`の返り値が`unknown`になるので`string`渡す
  return new Promise<string>(resolve => {
    setTimeout(() => {
      resolve("5秒経ちました");
    }, 5000);
  });
};

const hoge = "hoge";

で、その他の.tsファイルは空ファイル。

この状態でコンパイル結果は下記("outDir": "./dist"で、./dist配下だけ表示)。

./dist/
├── index.d.ts
├── index.js
└── src
    ├── hoge.d.ts
    ├── hoge.js
    └── piyo
        ├── foo
        │   ├── foo.d.ts
        │   └── foo.js
        ├── piyo.d.ts
        └── piyo.js
index.d.ts
export declare const func: () => Promise<string>;

index.jsのうち、funcの型定義が出力されている。

試しにindex.tsconst hoge = ~~~exportしてからコンパイルすると、

index.ts
console.log("hoge");

export const func = async () => {
  return new Promise<string>(resolve => {
    setTimeout(() => {
      resolve("5秒経ちました");
    }, 5000);
  });
};

export const hoge = "hoge";

結果は下記。

index.d.ts
export declare const func: () => Promise<string>;
export declare const hoge = "hoge";

exportすれば型定義ファイルの出力対象になる。

空ファイルの型定義ファイルは空ファイルになる:arrow_down:

piyo.d.ts

declarationMap

"declarationMap": true

Generates a sourcemap for each corresponding '.d.ts' file.

declarationオプションと併用するオプション。
これをtrueにすると、型定義のmapファイルが作成される(型定義ファイル自体にも、mapファイルの居場所が追記される)。
型定義のmapファイルというのは拡張子.d.ts.mapを持つファイルで、このmapファイルがあると、エディタで定義元にジャンプ(cmd + clickなど)した際に、型定義ファイルではなく実際のtsコードに飛ぶことができる。

詳しく

まず、最初に"declarationMap": falseな状態で、コンパイルする。

tsconfig.json
"declaration": true,
"declarationMap": false,

そして、新しく適当なtsファイルを作成し、今コンパイルしたfunc関数をimportする。

hoge.ts
import { func } from "./dist/index";

func();

スクリーンショット 2019-11-23 21.08.14.png

その状態でfunccmd + clickすると、

スクリーンショット 2019-11-23 21.08.58.png

ここ、つまり型定義ファイル(index.d.ts)に飛ぶ。

次に、"declarationMap": trueで再度コンパイル(hoge.tsはコンパイルに含めず)。

tsconfig.json
"declaration": true,
"declarationMap": true,
index.d.ts
export declare const func: () => Promise<string>;
export declare const hoge = "hoge";
//# sourceMappingURL=index.d.ts.map
index.d.ts.map
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,IAAI,uBAMhB,CAAC;AAEF,eAAO,MAAM,IAAI,SAAS,CAAC"}

結果は上記のように、型定義ファイルにmapファイルのパスのコメントが追加され、mapファイルも作成される。

この状態で、先ほどのhoge.tsfunccmd + clickすると

スクリーンショット 2019-11-23 21.12.27.png

ここ、つまり型定義ファイルではなく、実際のtsコードに飛ぶ。

sourceMap

"sourceMap": true

Generates corresponding '.map' file.

こちらは型定義ではなく、jsのmapファイル。

参考: https://developer.mozilla.org/ja/docs/Tools/Debugger/How_to/Use_a_source_map

詳しく

trueしてコンパイル("target": "es2019")した結果は下記。

index.js
console.log("hoge");
export const func = async () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("5秒経ちました");
    }, 5000);
  });
};
export const hoge = "hoge";
//# sourceMappingURL=index.js.map
index.js.map
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AAEpB,MAAM,CAAC,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;IAC7B,OAAO,IAAI,OAAO,CAAS,OAAO,CAAC,EAAE;QACnC,UAAU,CAAC,GAAG,EAAE;YACd,OAAO,CAAC,SAAS,CAAC,CAAC;QACrB,CAAC,EAAE,IAAI,CAAC,CAAC;IACX,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC"}

outFile

"outFile": "./dist/bundle.js"

Concatenate and emit output to single file.

これをtrueにすると、コンパイル結果を一つのファイルにまとめる。
バンドラ的機能。
ただし、これを使用するには、moduleオプションがamdまたはsystemである必要がある。
outFileを指定した場合はoutDirオプションは無視される。

詳しく

まずは、"target": "es2019"でかつ"module"は未指定(targtees2019なのでmoduleの値はデフォルト値のes6)でコンパイル。

tsconfig.json
"target": "es2019",
"outFile": "./dist/bundle.js"
index.ts:3:1 - error TS6131: Cannot compile modules using option 'outFile' unless the '--module' flag is 'amd' or 'system'.

amdsystemにしろと怒られる。

次は、moduleamdにしてコンパイル。

tsconfig.json
"target": "es2019",
"module": "amd",
"outFile": "./dist/bundle.js"
index.ts
import { hoge } from "./src/hoge";

export const func = async () => {
  return new Promise<string>(resolve => {
    setTimeout(() => {
      resolve("5秒経ちました");
    }, 5000);
  });
};

console.log(hoge);

参考のために./src/hoge.tsindex.tsからimport

src/hoge.ts
export const hoge: string = "HOGE";

結果は下記。

/dist/bundle.js
"use strict";
define("src/hoge", ["require", "exports"], function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.hoge = "HOGE";
});
define("index", ["require", "exports", "src/hoge"], function (require, exports, hoge_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.func = async () => {
        return new Promise(resolve => {
            setTimeout(() => {
                resolve("5秒経ちました");
            }, 5000);
        });
    };
    console.log(hoge_1.hoge);
});

次は、modulesystemにして同じコードをコンパイル。

tsconfig.json
"target": "es2019",
"module": "system",
"outFile": "./dist/bundle.js"

結果は下記。

bundle.js
"use strict";
System.register("src/hoge", [], function (exports_1, context_1) {
    "use strict";
    var hoge;
    var __moduleName = context_1 && context_1.id;
    return {
        setters: [],
        execute: function () {
            exports_1("hoge", hoge = "HOGE");
        }
    };
});
System.register("index", ["src/hoge"], function (exports_2, context_2) {
    "use strict";
    var hoge_1, func;
    var __moduleName = context_2 && context_2.id;
    return {
        setters: [
            function (hoge_1_1) {
                hoge_1 = hoge_1_1;
            }
        ],
        execute: function () {
            exports_2("func", func = async () => {
                return new Promise(resolve => {
                    setTimeout(() => {
                        resolve("5秒経ちました");
                    }, 5000);
                });
            });
            console.log(hoge_1.hoge);
        }
    };
});

rootDir

"rootDir": "./src"

Specify the root directory of input files. Use to control the output directory structure with --outDir.

コンパイル結果をoutDirで出力する際に、どのディレクトリ配下のディレクトリ構造で出力するかを指定する。
filesincludeとは違い、コンパイル対象を決めるオプションではない。
逆に、それらのオプションで決まっているコンパイル対象となるファイルは、全てrootDirで指定するディレクトリ配下に存在する必要がある。

詳しく

例えば下記のディレクトリ構造で、rootDir./srcにしたとする。

.
├── index.ts
├── package-lock.json
├── package.json
├── src
│   ├── hoge.ts
│   └── piyo
│       ├── foo
│       │   └── foo.ts
│       └── piyo.ts
└── tsconfig.json
tsconfig.json
"outDir": "./dist",
"rootDir": "./src"

filesincludeを指定していないので、コンパイル対象はtsconfig.jsonの存在するディレクトリ配下の全ての.ts.tsxファイル。この状態でコンパイルを実行すると、コンパイル対象がrootDirで指定したディレクトリ外に存在するため、以下のようにエラーが出る。

error TS6059: File '/path/to/index.ts' is not under 'rootDir' '/path/to/src'. 'rootDir' is expected to contain all source files.

これは、実際のルートディレクトリに存在するindex.tsが、コンパイル対象にも関わらず、rootDirで指定しているディレクトリ./src配下に存在しないため。

includeでコンパイル対象を./src/**/*にすれば、rootDirに全てのコンパイル対象が含まれるためコンパイルが通る。

tsconfig.json
"include": [
  "src/**/*"
]

コンパイル結果は下記(./distのみ記載)。

dist/
├── hoge.js
└── piyo
    ├── foo
    │   └── foo.js
    └── piyo.js

この出力結果は、rootDirで指定している./src配下と同じディレクトリ構成。

incremental

"incremental": true

Enable incremental compilation

公式:https://devblogs.microsoft.com/typescript/announcing-typescript-3-4/

このオプションをtrueにすると、以前コンパイルを実行したコードと現在のコードとの差分を検出して、必要なファイルだけをコンパイルするようになる。
以前コンパイルを実行したコードの状態を保存するために、tscは出力先ディレクトリの.tsbuildinfoファイルを利用する。
tscはこのファイルを自動で生成し、コンパイルした際には自動で更新するため、エンジニアが手動で変更を加えることはない。

trueの場合、初回コンパイルの時間は長くなる(コンパイル状況を保存する処理が入るため)が、その後のコンパイルは差分のみを対象とするため早くなる。

ただし、開発の途中でtsconfig.jsonの設定を変えた場合などは、差分だけコンパイルしていると以前コンパイルしたコードが最新のtsconfig.jsonの設定に準拠しないコードになる可能性があるので注意。
tsconfig.jsonを変えたら、一度.tsBuildInfoを削除して1からコンパイルし直した方が良い。

tsBuildInfoFile

"tsBuildInfoFile": "./dist/.diff"

Specify file to store incremental compilation information

incrementalオプションをtrueにした際に、.tsbuildinfoファイルの場所を自分で指定したい場合にそのパスを指定する。
ファイル名を変更することも可能。

removeComments

"removeComments": true

Do not emit comments to output.

コンパイル結果の出力ファイルから、コンパイル対象のファイル上のコメントを削除する。
ただし、著作権表示を表す/*!から始まるファイル先頭のコメントは、その直下に空行が存在する限り保持される。

/*!
 * Copyright 2019 Hoge
 */

export const Hoge = "HOGE";

参考: https://github.com/Microsoft/TypeScript/issues/12307

noEmit

"noEmit": true

Do not emit outputs.

trueにするとコンパイル結果を出力しなくなる。
tscによる型チェックだけを機能として利用したい場合(Babelなど他ツールが実際のコンパイルを行う場合)に使用する。

importHelpers

"importHelpers": true

Import emit helpers from 'tslib'.

コンパイル結果にPolyfillが必要な場合、出力結果のjs上でそれら定義するのではなく、tslibからimportすることで出力結果のファイルサイズを削減する。

$ npm install --save tslib

が必要。

詳しく

例えば、下記のコードを、targetオプションなし(値はデフォルト値のES3になる)でコンパイルしてみます。

index.ts
export const func = async () => {
  return new Promise<string>(resolve => {
    setTimeout(() => {
      resolve("5秒経ちました");
    }, 5000);
  });
};

すると、結果は下記のように、ES3に準拠した形でコードを再現するためのPolyfillが上半分以上で定義されているのが分かります。

index.js
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
// ここまでPolyfillの定義
exports.__esModule = true;
exports.func = function () { return __awaiter(void 0, void 0, void 0, function () {
    return __generator(this, function (_a) {
        return [2, new Promise(function (resolve) {
                setTimeout(function () {
                    resolve("5秒経ちました");
                }, 5000);
            })];
    });
}); };

これは、1ファイルだけならまだ良いですが、複数のファイルで同じようにPolyfillが必要な記法を使用すると、全てのファイルにこの定義のコードが生成されてしまいます。
こういったPolyfillが必要な環境というのはだいたいの場合古いバージョンのブラウザなど、良いとは言えない環境であり、そういった環境においてjsコードのファイルサイズというのは重要なポイントになります。

そこで、このオプションをtrueにしてコンパイルした結果がこちらです:arrow_down:

index.js
"use strict";
exports.__esModule = true;
var tslib_1 = require("tslib");
exports.func = function () { return tslib_1.__awaiter(void 0, void 0, void 0, function () {
    return tslib_1.__generator(this, function (_a) {
        return [2, new Promise(function (resolve) {
                setTimeout(function () {
                    resolve("5秒経ちました");
                }, 5000);
            })];
    });
}); };

Polyfillの定義が消え、

index.js
var tslib_1 = require("tslib");

が追加されています。
つまり、ファイルごとに定義するのではなく、モジュールとして切り出してそれを参照するという形になりました。

もちろん、requireするということはそのモジュールが必要なので、これを有効にする場合はtslibをインストールします。

$ npm install --save tslib

downlevelIteration

"downlevelIteration": true

Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'.

targetES3またはES5の時に、ジェネレータのyield*for..of構文などのイテレータを使用した記法を配列・文字列以外で使用する際にtrueにする。
TypeScript 2.3で導入。

詳しく

ES6で導入されたイテレータですが、配列・文字列以外のiterableなオブジェクトに対してイテレータに準拠した記法を実行しようとすると、targetES3ES5の場合にエラーになります。

// これは配列なのでOK
const arr = [1, 2, 3];
for (const value of arr) {
  console.log(value);
}

// Setはiterableだけど配列ではないのでエラー
const map = new Set([1, 2, 3]);
for (const value of map) {
  console.log(value);
}
error TS2569: Type 'Set<number>' is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators.

ジェネレータのyield*を配列・文字列以外で使用すると同様のエラーが出ます。

// これは配列なのでOK
function* func() {
  while (true) {
    const arr = [1, 2, 3];
    yield* arr;
  }
}

// Mapはiterablだけど配列・文字列ではないのでエラー
function* func() {
  while (true) {
    const map = new Map<string, string>();
    map.set("hoge", "HOGE");
    yield* map;
  }
}
error TS2569: Type 'Map<string, string>' is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators.

ちなみに、これは自作のiterableなオブジェクトでも同様です。

class MyIterator {
  count = 0;
  max: number;

  constructor(max: number) {
    this.max = max;
  }

  next() {
    const done = this.count > this.max;
    const value = done ? undefined : this.count;

    this.count = this.count + 1;

    return { value, done };
  }
}

const iterator = new MyIterator(10);

const iterable = {
  [Symbol.iterator]() {
    return iterator;
  }
};

for (const value of iterable) {
  console.log(value);
}

function* func() {
  while (true) {
    yield* iterable;
  }
}

イテレータもiterableなオブジェクトも自分で書いたとして、このコードのコンパイル結果は

error TS2569: Type '{ [Symbol.iterator](): MyIterator; }' is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators.

です。

これらは、"downlevelIteration": trueにすることでコンパイルが通るようになります。

isolatedModules

"isolatedModules": true

Transpile each file as a separate module (similar to 'ts.transpileModule').

コンパイル対象のファイル間の関係性を一切無視して、全てのファイルを単一のモジュールとしてコンパイルする。
その場合の安全でない記法についてコンパイル時にエラーを出すようにする。

これをtrueにした場合、コンパイル対象の全てのファイルがexport構文を含む必要があり、コード中の

  • declare const enum
  • 型のre-export

はエラーとなりコンパイル出来ない。

詳しく

このオプションの提案のissue: https://github.com/microsoft/TypeScript/issues/2499
このオプションが追加されたPR: https://github.com/microsoft/TypeScript/pull/2550/

isolatedModulestrueにすると、何もexportしていないファイルは、コード先頭で以下のようなエラーが出る。

const hoge = "hoge";
error TS1208: All files must be modules when the '--isolatedModules' flag is provided.

また、declare const enumと型のre-exportもエラーになる。

hoge.ts
export type Hoge = string;
index.ts
declare const enum FooBar {
  Foo = "foo",
  Bar = "bar"
}

export const str = `hoge: ${FooBar.Foo}`;

export { Hoge } from "./hoge";
error TS2748: Cannot access ambient const enums when the '--isolatedModules' flag is provided.

error TS1205: Cannot re-export a type when the '--isolatedModules' flag is provided.

re-exportに関しては、下記2点の回避策(参考: https://github.com/facebook/create-react-app/issues/6054)がある。

1: re-export時のexportexport *に変える。

index.ts
// export { Hoge } from "./src";

export * from "./src";

2: re-export時に再定義する。

index.ts
// export { Hoge } from "./src";

import { Hoge } from "./src";
export type ReExportedHoge = Hoge;

Next.js 9とisolatedModules

Next.jsでは、9からデフォルトでTypeScriptをサポートするようになった。
しかし、その方法はtscではなくBabel(babel/preset-typescript)を用いたトランスパイルであり、それに伴い"isolatedModules": trueが必須になった。
今まで独自にtscでTypeScriptをコンパイルしてNextを使っている(かつ"isolatedModules": falseである)場合、使用しているdeclare const enumや型のre-exportが全てエラーになり、コンパイルが通らなくなる。
とにかくこれはもう書き直すしかない...

strict

"strict": true

Enable all strict type-checking options.

このオプション自体は特定の機能を有効にするものではなく、このオプションをtrueにすると、下記のオプションが全てtrueになる。

  • --noImplicitAny
  • --noImplicitThis
  • --alwaysStrict
  • --strictBindCallApply
  • --strictNullChecks
  • --strictFunctionTypes
  • --strictPropertyInitialization

逆に言えば、stricttrueにした上で、任意のルールを一つずつfalseにすることが可能。

noImplicitAny

"noImplicitAny": true

Raise error on expressions and declarations with an implied 'any' type.

暗黙的にanyになる値をエラーにする。

詳しく
obj.ts
const obj = {};
const hoge = obj["hoge"];

const arr = [];
const item = arr[0];

このコードの定数hogeは型がanyになる。
また、arrany[]になる。

これらはこのオプションをtrueにすると以下のようなエラーでコンパイルが失敗する。

index.ts:14:14 - error TS7053: Element implicitly has an 'any' type because expression of type '"hoge"' can't be used to index type '{}'.
  Property 'hoge' does not exist on type '{}'.

14 const hoge = obj["hoge"];
                ~~~~~~~~~~~

index.ts:16:7 - error TS7034: Variable 'arr1' implicitly has type 'any[]' in some locations where its type cannot be determined.

16 const arr1 = [];
         ~~~~

index.ts:17:14 - error TS7005: Variable 'arr1' implicitly has an 'any[]' type.

17 const item = arr1[0];

回避するためには、以下のように明示的に型を書く。

hoge.ts
const obj: Record<string, number> = {};
const hoge = obj["hoge"];

const arr1: string[] = [];
const item = arr1[0];

あとは、下記のように関数の引数なども。

noImplicitAny - TypeScript Deep Dive 日本語版

anyになって良いことは何一つ無いので、必ずtrueで良いと思う。

strictNullChecks

"strictNullChecks": true

Enable strict null checks.

Nullableな値に対してプロパティの呼び出しを行う記述をエラーにする。

詳しく
type Obj = {
  hoge?: string;
};

const obj: Obj = {};
const trimmed = obj.hoge.trim();

trimString.prototype.trim

このObj型のプロパティhoge?付きなのでundefinedが入りうる。
hogeundefinedの場合、trimを呼ぶとランタイムエラーになる。

このオプションがtrueの場合は、以下のようにコンパイル時にエラーになる。

index.ts:25:17 - error TS2532: Object is possibly 'undefined'.

25 const trimmed = obj.hoge.trim();

これを回避するためには、値がundefinedではないことを保証する。例えば、

const obj: Obj = {};
if (obj.hoge != undefined) {
  const trimmed = obj.hoge.trim();
}

など。

ちなみに、!Non-Null Assertion Operator)でもエラーを消すことができるが、実質ランタイムエラーになるだけなので、基本的にやってはいけない。
唯一許容できるとすれば、コードの論理上、非undefinedであることが定まっているが、tscの型推論が効かない場合のみ。
例えば下記のようなコード。

const component = props.func && (
  <div onClick={() => props.func!()}>
    contents
  </div>
);

この場合、props.func &&を通っている時点でprops.funcが非undefinedであることは確定しているが、jsxの中で渡そうとすると型推論が効かず、!をつけないとtscでエラーになる。
とは言え、!を使わなくて良いようなコードにすることを心がけたい。

strictFunctionTypes

"strictFunctionTypes": true

Enable strict checking of function types.

公式:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html

関数代入時の引数の型チェックにおいて、TypeScriptのデフォルトはBivariantlyな挙動だが、このオプションをtrueにするとContravariantlyに型チェックが走るようになる。

Varianceについては下記の記事が参考になります。

詳しく

Varianceとは、端的に言えば「型の違う変数同士を代入する際のルール」のことです。
(ちなみにこの概念自体はTypeScript固有のものではなく、プログラミング言語において一般的に使用される概念です。)

ここで言う「型の違う」というのは、基本的には継承関係にある親子のクラス型間の話です。
つまり、stringnumberは継承関係に無いので、それらの型を持った変数同士が互いに代入不可能なのは当然です。

しかし、継承関係にある変数というのは、型が違っても代入可能な場合があります。
オブジェクト指向でポリモーフィズムと呼んでいる仕組みがまさにそれで、親クラス型の変数には、子クラス型の変数を代入することでき、これによって柔軟なコードを書くことができるようになります(下記のコードは仮想の言語で、雰囲気)。

class SmartPhone {
  call() { /* 電話をかける処理(このクラスを継承する子クラス全てに共通の、処理またはインターフェース) */ }
}
class iOSSmartPhone extends SmartPhone {
  purchaseByAppStore() { /* AppStoreで課金する処理(子クラスにしか存在しない処理) */ }
}
class AndroidSmartPhone extends SmartPhone {
  purchaseByPlayStore() { /* PlayStoreで課金する処理(子クラスにしか存在しない処理) */ }
}

iOSSmartPhone iosSp = new iOSSmartPhone();
AndroidSmartPhone androidSp = new AndroidSmartPhone();

SmartPhone[] sps = [ iosSp, androidSp ];

// OSが異なるスマホでも、callメソッドは共通しているため全部一括でcallメソッドを実行できる。
for (int i = 0; i < sps.length; i++) {
  sps[i].call();
}

この、「親クラス型の変数には子クラス型の変数を代入できるルール」のことを、VarianceではCovariant(もしくはCovariance)と呼びます。
VarianceにはCovariantを含めた4つの種類が存在します。

  1. Covariant/Covariance: 親クラス型の変数には、子クラス型の変数を代入できる。
  2. Contravariant/Contravariance: 子クラス型の変数には、親クラス型の変数を代入できる。
  3. Bivariant/Bivariance: 継承関係にあるクラス同士であれば、親でも子でも互いに代入できる。
  4. Invariant/Invariance: 継承関係にあっても、型が異なれば代入はできない。

話を戻すと、TypeScriptでは関数代入時に引数の型チェックの挙動はデフォルトでBivariantです。

ちなみに、関数同士の代入において、下記の条件は必須になります。

  1. 関数の返り値は、代入先の関数の型の返り値型を全て満たしている。
    • 余分にある分には構わない。
  2. 引数の数は、代入先の関数の引数の数以上である。
    • 多い分には構わない。
    • オプション引数(?つき)でも、個数を満たしていれば代入可能。
    • 可変長引数の場合は相手の引数に数に関わらず代入可能。

感覚的にはCovariantじゃないんだ、と思うんですが、実際に書いてみると関数の引数に関してはCovariantが危険であることがわかります。

class SmartPhone {
  // このクラスを継承するクラス全てに共通のメソッド
  call() {
    console.log("Calling...");
  }
}

class iOSSmartPhone extends SmartPhone {
  // 子クラス特有のメソッド
  openAppStore() {
    console.log("Opened!");
  }
}

let openAppStore: (sp: iOSSmartPhone) => void = sp => sp.openAppStore();
let callBySmartPhone: (sp: SmartPhone) => void = sp => sp.call();

の時に、

// デフォルトではOK、`strictFunctionTypes: true`の時にはError
callBySmartPhone = openAppStore;
// RuntimeError: 型定義的には親クラスのインスタンスを渡すのが正解だが、
//               実際の中身の処理ではそのインスタンスに対して子クラス特有のメソッドを呼び出しているため
callBySmartPhone(new SmartPhone());
// OK
openAppStore = callBySmartPhone;
// OK: 型定義的には子クラスのインスタンスを渡すが、
//     中の実際の処理では親クラスのメソッドを呼び出している。特に問題なし。
openAppStore(new iOSSmartPhone());

となるからです。

strictFunctionTypestrueにすることで、この危険なCovariantな関数代入を静的型チェックの段階でエラーにすることが出来ます。

TypeScriptは自由を求めてデフォルトでこの手のルールではゆるい方の挙動を取るけど、実際にはランタイムエラーを起こしうるので静的にチェックしてほしいところ。
つまり、これもとりあえずtrueにしておきましょう。

そもそもの話、Immutableを意識してconstだけ使っていれば通常気にする必要は無い話ではあると思うけど...

ちなみに(Contravariantも危険だっていう話)

実は、代入が参照渡しの時点で、Contravariantもランタイムエラーを引き起こす可能性があります。

https://typescript-jp.gitbook.io/deep-dive/type-system/type-compatibility

ここを読んでいて面白かったので紹介します。

animalArr = catArr; // Okay if covariant
animalArr.push(new Animal('another animal')); // Just pushed an animal into catArr!
catArr.forEach(c => c.meow()); // Allowed but BANG 🔫 at runtime

この部分。
CatAnimalクラスの子クラスで、animalArrcatArrはそれぞれAnimalの配列とCatの配列。

1行目で、親クラスの配列型に対して、子クラスの配列型を代入しています。
これはCovariantなので大丈夫なように感じるんですが、配列の場合参照渡しになるので、animalArr = catArr;の後にanimalArr.push()をすると、catArrにも要素が追加されます(というかどっちも同じ配列を参照してる)。

ので、その配列に親クラス(Animal)型の要素をpushした後に、うっかりcatArrに対してforEachで子クラス(Cat)特有のメソッドを呼び出すと、型定義は間違っていないのに、配列の中に親クラスのインスタンスが入っているため、ランタイムエラーになります。

ひえ〜

JavaScriptのようなミュータブル(変更可能)なデータの存在下で、完全に健全な型システムのためにはinvariantが唯一有効なオプションです。

ということですね。

strictBindCallApply

"strictBindCallApply": true

Enable strict 'bind', 'call', and 'apply' methods on functions.

bind, call, applyを使用する際に、より厳密に型チェックが行われるようになる。

公式ドキュメント: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-2.html

TODO: 詳しく

strictPropertyInitialization

"strictPropertyInitialization": true

Enable strict checking of property initialization in classes.

クラス定義時、インスタンス変数の初期化が宣言時、もしくはコンストラクタのどちらでも行われていない場合にエラーになる。
TypeScript2.7で導入。

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html

詳しく
index.ts
class C {
  foo: number;
  bar = "hello";
  baz: boolean;
  //  ~~~
  //  Error! Property 'baz' has no initializer and is not assigned directly in the constructor.
  constructor() {
    this.foo = 42;
  }
}
error TS2564: Property 'baz' has no initializer and is not definitely assigned in the constructor.

このbazは宣言時もコンストラクタでも初期化されていない。

エラーを回避するためには、

index.ts
  baz: boolean = false;

このように宣言時に初期化するか、もしくは

index.ts
  constructor() {
    this.foo = 42;
    this.baz = false;
  }

コンストラクタで初期化する。

また、!(definite assignment assertion modifier)を変数宣言時につけることで、強制的にエラーを消すことができる。

これは、DIなどで外部から代入されることを想定している場合などに役立つ。
Non-null Assertion Operatorをあまり使わないようにするのと同様に、基本的には初期化するという正当な方法でエラーを回避するべき。

noImplicitThis

"noImplicitThis": true

Raise error on 'this' expressions with an implied 'any' type.

使われているthisの型が暗黙的にanyになる場合にエラーにする。

詳しく
index.ts
const func = function() {
  console.log(this);
};

このコードは、下記のエラーが出ます。

error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.

functionによる関数の場合、thisの値は宣言時に定まらないため、暗黙的に型はanyになるから。

これを回避するためには2種類の方法がある。

1: アロー関数を使う

アロー関数の場合、thisはそのアロー関数の定義コンテキストのthisを参照するため、定義コンテキストでthisが定まる場合で、かつそれが意図している参照なのであればアロー関数にするだけで良い。

例えば、

index.ts
const obj = {
  count: 0,
  hoge: function(label: string) {
    return function() {
      console.log(`${label}: ${this.count}`);
    };
  }
};

(実用性は何も考えないとして)上記のようにfunctionによる関数をreturnしている場合、意図としてはobj.countを参照したいが、functionではbindを使わないとthisが定まらない。
(ちなみに、仮にこの返り値の関数をbind(this)してからreturnしてもエラーは消えない)

こういう場合はアロー関数にするだけで良い。

index.ts
const obj = {
  count: 0,
  hoge: function(label: string) {
    return () => {
      console.log(`${label}: ${this.count}`);
    };
  }
};

2: 明示的にthisの型を指定する。

TypeScriptでは、functionによる関数宣言で作られる関数の第一引数の名前がthisの場合、その引数の型は関数内のthisの型を指す(アロー関数にはこの機能は存在しない: 'An arrow function cannot have a 'this' parameter.ts(2730)')。
そしてその場合、2個目以降の引数が実際のその関数の引数になる。

例えば下記のような、thisconsole.logする関数があったとして、

index.ts
const logThis = function() {
  console.log(this);
};

これは

error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.

が出るのでコンパイル出来ない。
しかし、この関数はアロー関数に変えても別のエラーが出る。

error TS7041: The containing arrow function captures the global value of 'this'.

そこで、

index.ts
const logThis = function(this: object) {
  console.log(this);
};

このように引数でthisを指定すると、console.log(this);部分のthisは型が定まる(この場合はobject)。

この書き方をした場合は、2個目以降の引数が実際の関数の引数になるため、呼び出しは引数を何も取らない形で実行できる。

index.ts
const logThis = function(this: object) {
  console.log(`this`);
};

const obj = { logThis };

obj.logThis();

引数が欲しい場合はこう。

index.ts
const logThis = function(this: object, label: string) {
  console.log(`${label}: ${this}`);
};

const obj = { logThis };

obj.logThis("HOGEHOGE:");

出力されるjsを見れば通常の関数として扱われているのがわかる。

index.js
var logThis = function (label) {
    console.log(label + ": " + this);
};
var obj = { logThis: logThis };
obj.logThis("HOGEHOGE");

alwaysStrict

"alwaysStrict": true

Parse in strict mode and emit "use strict" for each source file.

"use strict";を必ず全てのファイルの先頭行に付与する。

参考: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Strict_mode

moduleResolution

"moduleResolution": "node"

Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6).

公式ドキュメント: https://www.typescriptlang.org/docs/handbook/module-resolution.html

tscのモジュール解決の方法を指定する。
値は"node""classic"
デフォルト値は、moduleの指定値がAMDSystemES2015のいずれかである場合はclassic、それ以外の場合はnode

classicが元々tsでデフォルトで使われていた方法ですが、主に後方互換のために残してあると明記してあるので、新しい環境でやる場合はnodeにしておきましょう。

詳しく

モジュール解決(Module Resolution)とは、tscがimport文を解釈する際に、そのimportが参照する定義ファイルを特定するプロセスのこと。

まず、モジュール解決にはrelativeとnon-relativeの2種類が存在する。

relative

import文のfromで指定する文字列が/./../のいずれかで始まる、つまりパスを指定している場合。
基本的にimportしたいファイルが直接そのレポジトリに入っている場合に使用する。

non-relative

それ以外、つまり通常npm installしたパッケージをimportする時のようにいきなり単語を指定している場合。
baseUrlで指定した先、もしくはpathsで指定したパスマッピングの先、そしてmoduleのアンビエント宣言はnon-relativeにだけ適用される。

その上で、tsでは名前解決の方法にnodeもしくはclassicという2種類が存在する。

classic(古い)

これは非常にそのままで、relativeの場合はその指定したパスにあるファイルを、non-relativeの場合は、指定した文字列.tsなファイルを現在の階層も含めてレポジトリのルートディレクトリまで1個ずつ遡って探す、という方法。

node_modulesとか一切関係なく、ひたすらファイル名だけが考慮される。現代では使用することほとんど無いと思うので説明あっさり。

node(現状のおすすめ)

nodeはNode.jsのモジュール解決を模倣している方法。

まずNodeがどうやって名前解決するかの説明。

Nodeにおける、relativeの場合

基本的には指定したファイルを読みにいく。
が、classicと違うのは、指定したパスがディレクトリの場合に、そのディレクトリのpackage.jsonでmainに指定したファイル、もしくはそのディレクトリ直下のindex.jsを読む、という点。
優先順位的には、 /src/path/to/a.tsimport ... from "./b"していた場合、

  1. /src/path/to/b.js
  2. /src/path/to/main.js/src/path/to/package.jsonが存在し、その中で"main": "main.js"という記述があった場合のみ)
  3. /src/path/to/index.js

Nodeにおける、non-relativeの場合

Nodeでは、non-relativeなimportはすなわちnode_modulesからのimportを意味する。
基本的な読み込みルールは上で書いたrelativeと変わらず(指定した単語.js、もしくはpackage.jsonとかindex.jsとか)、node_modulesというディレクトリの中のみ探索対象とする。
classicと同様、階層をどんどん上に遡って見ていく。

つまり、 /src/path/to/a.tsimport ... from "b"していた場合、

  1. /src/path/to/node_modules/b.js
  2. /src/path/to/node_modules/b/hoge.js(package.jsonがあって"main"で指定してたら)
  3. /src/path/to/node_modules/b/index.js
  4. /src/path/node_modules/b.js
  5. /src/path/node_modules/hoge.js(package.jsonがあって"main"で指定してたら)
  6. /src/path/node_modules/b/index.js
  7. /src/node_modules/b.js
  8. /src/node_modules/hoge.js(package.jsonがあって"main"で指定してたら)
  9. /src/node_modules/b/index.js
  10. /node_modules/b.js
  11. /node_modules/hoge.js(package.jsonがあって"main"で指定してたら)
  12. /node_modules/b/index.js

な感じ。

さて、やっとtsにおける読み込み順序の話。

tsにおける、relative

基本的にNodeと同じ。ただし、読み込むファイルの拡張子が.jsだけでなく

  1. .ts
  2. .tsx
  3. .d.ts

となる(上記は読み込む優先度順)。

さらに、package.jsonの"types"に指定したファイルを"main"に指定したファイルの型定義ファイルとして参照する。
(package.jsonの"main"にtsファイルが指定されることはない(あったとしたらインストール後にモジュール側のコンパイルも必要になってしまう)ので、ts的には"main"の値は関知しない)

パッケージで提供するモジュールの型定義は@types/xxxに置くこともできるが、公式にパッケージ自体がtsの型定義を持つのが利用者としては一番楽だ。
その方法が、package.jsonの"types"に型定義ファイルを指定する、もしくはルートディレクトリにindex.d.tsを置くこと。

tsにおける、non-relative

これも基本的にNodeと同じですが、relativeと同様に拡張子が3種類になる。
一点明確に違うのは、node_modules/@types配下の型定義ファイルも参照の対象になるという点。

優先順位的には

  1. /src/path/to/node_modules/b.ts
  2. /src/path/to/node_modules/b.tsx
  3. /src/path/to/node_modules/b.d.ts
  4. /src/path/to/node_modules/b/hoge.d.ts(package.jsonのtypesで指定されていたら)
  5. /src/path/to/node_modules/@types/b.d.ts
  6. /src/path/to/node_modules/b/index.ts
  7. /src/path/to/node_modules/b/index.tsx
  8. /src/path/to/node_modules/b/index.d.ts

baseUrl

"baseUrl": "./"

Base directory to resolve non-absolute module names.

non-relativeなimportにおいて、相対的なカレントディレクトリをどこにするか指定する。
値が./の場合はtsconfig.jsonが置いてあるディレクトリを指す。

relativeなimportには一切関係しない。

composite

"composite": true

Enable project compilation

Project Referencesを有効にする際に、referencesで指定されたtsconfig.jsonはこの値をtrueにする。

noUnusedLocals

"noUnusedLocals": true

Report errors on unused locals.

宣言されたが使用されていない変数が存在する場合にコンパイルエラーにする。
デフォルト値はfalse。とりあえずtrueにしておけ系。

開発中にめんどくさいみたいな時は一時的にfalseにしたりするかも。

noUnusedParameters

"noUnusedParameters": true

Report errors on unused parameters.

関数の作成時、定義しているのに中身のコードで使用されない場合にコンパイルエラーにする。
デフォルトfalse。とりあえずtrueにしておけ...系?少なくとも自分はtrueで良いと思う。

詳しく

使わない引数は書かなければ良いのだが、例えば第二引数は使うんだけど第一引数は使わない、みたいな場合はどうしても定義しないといけなくなる。
そういう場合は、使わない引数のprefixとして_アンダースコアを付与するとエラーを回避できる。
jsは仕組みで縛れないけど「使うなよ!絶対使うなよ!」っていう値にアンダースコアをつける文化ですね。
明示的に「これは使わない変数です」っていうのをわかりやすくすればOK、みたいなイメージ。

// これはhogeを関数内で使用していないのでコンパイルエラー
const func = (hoge: string, piyo: string) => {
  console.log("HOGE!!! and " + piyo);
};
// index.ts:1:15 - error TS6133: 'hoge' is declared but its value is never read.
// 使っていない第一引数にアンダースコアを追加したのでコンパイルが通る
const func = (_hoge: string, piyo: string) => {
  console.log("HOGE!!! and " + piyo);
};

// ちなみにアンダースコアだけでも通る
const func = (_: string, piyo: string) => {
  console.log("HOGE!!! and " + piyo);
};

noImplicitReturns

"noImplicitReturns": true

Report error when not all code paths in function return a value.

all code pathsっていうのは、条件分岐した場合に全ての状況で、という意味ですね。
つまり関数内で、条件分岐の条件によって明示的なreturnがされないルートがある場合、コンパイルエラーになります。

このエラーが出る場合、だいたいは設計ミスでもっと良い書き方がある(早期リターンとか関数の分離とか)気もするので、それに気づかせてくれるという意味でとりあえずtrueにしておけば良い気はします。

詳しく
// hogeがfalseの場合に明示的な`return`が無いのでコンパイルエラー
const func = (hoge: boolean) => {
  if (hoge) {
    return "HOGE!!!";
  }
};
// index.ts:1:14 - error TS7030: Not all code paths return a value.
// hogeがfalseの場合に明示的にundefinedを返しているのでコンパイルが通る
const func = (hoge: boolean) => {
  if (hoge) {
    return "HOGE!!!";
  }

  return;
};

ちなみに、関数の返り値の型を明示的に指定している場合は、このオプションがfalseであっても型が異なればエラーが出ます。

// "noImplicitReturns": false

// string型を返すことになっているが、hogeがfalseだった場合にundefinedが返るのでコンパイルエラー。
const func = (hoge: boolean): string => {
  if (hoge) {
    return "HOGE!!!";
  }
};
// index.ts:1:31 - error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
// "noImplicitReturns": false

// 返り値の型がundefinedとのユニオン型になったのでコンパイルが通る。
const func = (hoge: boolean): string | undefined => {
  if (hoge) {
    return "HOGE!!!";
  }
};

noFallthroughCasesInSwitch

"noFallthroughCasesInSwitch": true

Report errors for fallthrough cases in switch statement.

fallthroughというのはswitch文のcase内でbreakが無い場合に、その下のcaseの処理も実行される仕様のことです。
jsではfallthroughが発生するので、breakを書かないとどんどん下のcase文が実行されていきます。

これオプション名からわかりにくいんですが、fallthroughなcaseのうち、1行以上処理が存在しているにも関わらず脱出処理(breakやreturn)が無いものにエラーを吐きます。

とりあえずtrueにしておけ系。

詳しく

上で太字にした部分を説明します。

Rubyを書いているとcase文(jsで言うswitch文のこと)の中でwhen句(jsで言うcaseのこと)に複数の値を指定できるのが結構便利だったりします。jsではcase文に複数の値を指定できないので、そういう時はswitch文のfallthroughを利用したくなります。

// "noFallthroughCasesInSwitch": true
const func = (hoge: string) => {
  switch (hoge) {
    case "HOGE":
    case "HOGE!!!":
    case "HOGE??":
    case "HOGE~~~~":
      return "This is HOGE!!!";
    case "piyo":
    case "PIYO":
    case "PIYO!!!":
      return "This is PIYO!!!";
  }
};

この書き方はjs的にはなんら問題なく動きますし、"noFallthroughCasesInSwitch": trueであってもtsのコンパイルも通ります。
この例の場合、複数条件指定のつもりで書いているfallthroughなcaseには処理が1行もありません。
ので、上で太字にした「1行以上処理が存在しているのに脱出処理が無い」という条件に当てはまりません。

逆に、下記のコードはコンパイルエラーです。

// "noFallthroughCasesInSwitch": true
const func = (hoge: string) => {
  let result;

  switch (hoge) {
    case "HOGE":
    case "HOGE!!!":
      console.log("HOGE!!"); // 1行以上の処理があるのにbreakもreturnも無いのでエラー
    case "HOGE??":
    case "HOGE~~~~":
      return "This is HOGE!!!";
    case "piyo":
    case "PIYO":
      result = "PIYO"; // 1行以上の処理があるのにbreakもreturnも無いのでエラー
    case "PIYO!!!":
      return "This is PIYO!!!";
  }

  return result;
};
index.ts:7:5 - error TS7029: Fallthrough case in switch.

7     case "HOGE!!!":
      ~~~~~~~~~~~~~~~

index.ts:13:5 - error TS7029: Fallthrough case in switch.

13     case "PIYO":
       ~~~~~~~~~~~~


Found 2 errors.

fallthroughは嫌いな人も多いわけですが、「複数の条件を指定したい場合」に限って言えば許されるようです。
個人的にも、fallthroughを複数条件指定以外で使う蓋然性が高いコードというのは見たことが無いので、このオプションはtrueで良いと思います。

この先工事中...

Next:

  • paths
  • rootDirs
  • typeRoots
  • types
  • allowSyntheticDefaultImports
  • esModuleInterop
  • preserveSymlinks
  • allowUmdGlobalAccess
  • sourceRoot
  • mapRoot
  • inlineSourceMap
  • inlineSources
  • experimentalDecorators
  • emitDecoratorMetadata
  • forceConsistentCasingInFileNames
ryokkkke
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした