概要
社内勉強会の資料。
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",
]
コンパイルするファイルを直接指定する。
こちらはinclude
やexclude
と違ってワイルドカードを使用できない。
拡張子は省略可能。省略した場合、.ts
ファイルが優先して対象になる(hoge.ts
とhoge.tsx
がある際に"hoge"
を指定すると.ts
ファイルだけがコンパイルされる)。
include
コンパイルする対象ファイルを記述する。
ワイルドカード(*
,?
,**/
)が使える。
- * 0個以上の文字に一致します(ディレクトリ区切り文字を除く)
- ? 任意の1文字に一致します(ディレクトリ区切り文字を除く)
- **/ 任意のサブディレクトリに再帰的に一致します
"include": [
"src/**/*"
]
上記の場合は、src配下のファイル全て(ディレクトリのネストが深くなっても再帰的に全て)がコンパイル対象。
拡張子を指定しない(*
のみ)、もしくは拡張子にアスタリスクを使用する(hoge.*
)場合、デフォルトでは下記の拡張子のファイルのみが対象になる。
.ts
.tsx
.d.ts
ただし、compilerOptions.allowJs
がtrue
の場合は、下記の拡張子も含む。
.js
.jsx
files
もinclude
も指定しない場合、tsconfig.json
が置かれているディレクトリ配下の全てのTypeScriptファイル(拡張子が.ts
、.d.ts
、.tsx
であるファイル)のうち、exclude
に含まれるファイル以外がコンパイル対象になる。
exclude
include
で指定したファイルから特別に除外するファイルを記述する。
include
と同じワイルドカードが使える。
(2022/04/02)公式のドキュメントでも強調されているのですが、この exclude
は 「指定したファイルを tsc が読み込まないようにする」オプションではありません。 あくまでも、 include
で指定したファイルの中から除外するファイルを指定するだけです。つまり、エントリーファイルから exclude するだけで、エントリーした後のファイルには関知しません。
なので、例えば exclude
に hoge.ts
を指定しても、 include
に含まれている piyo.ts
が hoge.ts
を import
していれば tsc は hoge.ts
を読み込みます。
下記ドキュメントをご確認ください。
exclude
は指定しない場合デフォルトで以下の値を含む。
node_modules
bower_components
jspm_packages
-
outDir
オプションで指定しているディレクトリ配下のファイル
逆に言うと、tsconfig.json
にexclude
オプションが指定されている場合は、outDir
の中身も対象になってしまう。
詳しく
"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コードということになる。
詳しく
"target": "es3"
const func = async () => {
return new Promise(resolve => {
setTimeout(() => {
resolve("5秒経ちました");
}, 5000);
});
};
だとしましょう。
これをコンパイルした結果はこちら。
コンパイル結果
"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"
にしてコンパイルした結果がこちら。
コンパイル結果
"use strict";
const func = async () => {
return new Promise(resolve => {
setTimeout(() => {
resolve("5秒経ちました");
}, 5000);
});
};
これはつまり、es3
に準拠させようとするとasyncやPromiseなんて存在しないので、レガシーなjsコードでPromiseやasyncを再現するPolyfilが手前で生成されていて、それを使用して元のコードを再現している、そしてes2019
に準拠させる場合は、asyncやPromiseはすでに標準で組み込まれているので、そのまま出力されるということです。
ここはTypeScriptのコンパイルというよりはBabel的な役割ですね。
lib
"lib": ["dom", "es2019"]
Specify library files to be included in the compilation.
コンパイルする際に使用する組み込みライブラリを指定する。
基本的にはtarget
で指定しているjsのバージョンに含まれているものは暗黙的に指定される。
ただし、target
に指定しているjsのバージョンには含まれていない組み込みライブラリを使用する場合は、明示的な指定が必要。
詳しく
例えば、tsconfig.json
でtarget
を指定しない場合、デフォルトではES3
になるため、ES3に含まれていない組み込みライブラリはエラーになる。
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
に
"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のモジュールの仕組みとして何を使用するかを指定する。
詳しく
export const func = async () => {
return new Promise(resolve => {
setTimeout(() => {
resolve("5秒経ちました");
}, 5000);
});
};
"module": "commonjs"
の場合、コンパイル結果は
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.func = async () => {
return new Promise(resolve => {
setTimeout(() => {
resolve("5秒経ちました");
}, 5000);
});
};
"module": "es2015"
の場合、コンパイル結果は
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
"jsx": "preserve"
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)
の場合は
"use strict";
var Component = function () { return (<section>
<h1>Component</h1>
<p>Contents</p>
</section>); };
こうなる。preserve
はjsxをjsxのまま保持するので、拡張子は.jsx
になる。
jsxのコンパイルをtscではなくBabelなど他に任せたい場合に使用する。
Reactのコードにコンパイルしたい場合は下記のように"react"
を指定する。
"jsx": "react"
とりあえずコンパイルだけなので@types/react
だけインストール。
$ npm install --save-dev @types/react
const Component = () => (
<section>
<h1>Component</h1>
<p>Contents</p>
</section>
);
@types/react
がインストールされていればjsxの型についてこちらが書く必要は無いのでこれだけ。
"use strict";
var Component = function () { return (React.createElement("section", null,
React.createElement("h1", null, "Component"),
React.createElement("p", null, "Contents"))); };
結果はこれ。jsxの記法が完全に消え、拡張子も.js
になる。
"jsx": "react-native"
react-native
は使う予定が無いので一旦パス。
declaration
"declaration": true
Generates corresponding '.d.ts' file.
これをtrue
にすると、コンパイルしたtsファイルの中でexport
しているもの全ての型定義ファイルをファイルごとに作成する。
export
が一つもなくてもファイル自体は作成される。
詳しく
"declaration": true
.
├── index.ts
├── package-lock.json
├── package.json
├── src
│ ├── hoge.ts
│ └── piyo
│ ├── foo
│ │ └── foo.ts
│ └── piyo.ts
└── tsconfig.json
こういうディレクトリ構成だとして、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
export declare const func: () => Promise<string>;
index.js
のうち、func
の型定義が出力されている。
試しにindex.ts
のconst hoge = ~~~
をexport
してからコンパイルすると、
console.log("hoge");
export const func = async () => {
return new Promise<string>(resolve => {
setTimeout(() => {
resolve("5秒経ちました");
}, 5000);
});
};
export const hoge = "hoge";
結果は下記。
export declare const func: () => Promise<string>;
export declare const hoge = "hoge";
export
すれば型定義ファイルの出力対象になる。
空ファイルの型定義ファイルは空ファイルになる。
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
な状態で、コンパイルする。
"declaration": true,
"declarationMap": false,
そして、新しく適当なtsファイルを作成し、今コンパイルしたfunc
関数をimport
する。
import { func } from "./dist/index";
func();
その状態でfunc
をcmd + click
すると、
ここ、つまり型定義ファイル(index.d.ts
)に飛ぶ。
次に、"declarationMap": true
で再度コンパイル(hoge.ts
はコンパイルに含めず)。
"declaration": true,
"declarationMap": true,
export declare const func: () => Promise<string>;
export declare const hoge = "hoge";
//# sourceMappingURL=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.ts
のfunc
をcmd + click
すると
ここ、つまり型定義ファイルではなく、実際の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"
)した結果は下記。
console.log("hoge");
export const func = async () => {
return new Promise(resolve => {
setTimeout(() => {
resolve("5秒経ちました");
}, 5000);
});
};
export const hoge = "hoge";
//# sourceMappingURL=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"
は未指定(targte
がes2019
なのでmodule
の値はデフォルト値のes6
)でコンパイル。
"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'.
amd
かsystem
にしろと怒られる。
次は、module
をamd
にしてコンパイル。
"target": "es2019",
"module": "amd",
"outFile": "./dist/bundle.js"
import { hoge } from "./src/hoge";
export const func = async () => {
return new Promise<string>(resolve => {
setTimeout(() => {
resolve("5秒経ちました");
}, 5000);
});
};
console.log(hoge);
参考のために./src/hoge.ts
をindex.ts
からimport
。
export const hoge: string = "HOGE";
結果は下記。
"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);
});
次は、module
をsystem
にして同じコードをコンパイル。
"target": "es2019",
"module": "system",
"outFile": "./dist/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
で出力する際に、どのディレクトリ配下のディレクトリ構造で出力するかを指定する。
files
やinclude
とは違い、コンパイル対象を決めるオプションではない。
逆に、それらのオプションで決まっているコンパイル対象となるファイルは、全てrootDir
で指定するディレクトリ配下に存在する必要がある。
詳しく
例えば下記のディレクトリ構造で、rootDir
を./src
にしたとする。
.
├── index.ts
├── package-lock.json
├── package.json
├── src
│ ├── hoge.ts
│ └── piyo
│ ├── foo
│ │ └── foo.ts
│ └── piyo.ts
└── tsconfig.json
"outDir": "./dist",
"rootDir": "./src"
files
やinclude
を指定していないので、コンパイル対象は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
に全てのコンパイル対象が含まれるためコンパイルが通る。
"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
になる)でコンパイルしてみます。
export const func = async () => {
return new Promise<string>(resolve => {
setTimeout(() => {
resolve("5秒経ちました");
}, 5000);
});
};
すると、結果は下記のように、ES3に準拠した形でコードを再現するためのPolyfillが上半分以上で定義されているのが分かります。
"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
にしてコンパイルした結果がこちらです。
"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の定義が消え、
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'.
target
がES3
またはES5
の時に、ジェネレータのyield*
やfor..of
構文などのイテレータを使用した記法を配列・文字列以外で使用する際にtrue
にする。
TypeScript 2.3で導入。
詳しく
ES6で導入されたイテレータですが、配列・文字列以外のiterableなオブジェクトに対してイテレータに準拠した記法を実行しようとすると、target
がES3
・ES5
の場合にエラーになります。
// これは配列なので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/
isolatedModules
をtrue
にすると、何もexport
していないファイルは、コード先頭で以下のようなエラーが出る。
const hoge = "hoge";
error TS1208: All files must be modules when the '--isolatedModules' flag is provided.
また、declare const enum
と型のre-exportもエラーになる。
export type Hoge = string;
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点の回避策(参考:create-react-app#6054)がある。
1: re-export時のexport
をexport *
に変える。
// export { Hoge } from "./src";
export * from "./src";
2: re-export時に再定義する。
// 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
逆に言えば、strict
をtrue
にした上で、任意のルールを一つずつfalse
にすることが可能。
noImplicitAny
"noImplicitAny": true
Raise error on expressions and declarations with an implied 'any' type.
暗黙的にany
になる値をエラーにする。
詳しく
const obj = {};
const hoge = obj["hoge"];
const arr = [];
const item = arr[0];
このコードの定数hoge
は型がany
になる。
また、arr
はany[]
になる。
これらはこのオプションを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];
回避するためには、以下のように明示的に型を書く。
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();
trim
はString.prototype.trim。
このObj型のオプションhoge
は?
付きなのでundefined
が入りうる。
hoge
がundefined
の場合、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については下記の記事が参考になります。
- ジェネリックの共変性と反変性 - Microsoft .NET
- Type Compatibility - TypeScript Deep Dive 日本語版
- TypeScript 2.6 変更点と注意点 - abcdefGets
詳しく
Varianceとは、端的に言えば「型の違う変数同士を代入する際のルール」のことです。
(ちなみにこの概念自体はTypeScript固有のものではなく、プログラミング言語において一般的に使用される概念です。)
ここで言う「型の違う」というのは、基本的には継承関係にある親子のクラス型間の話です。
つまり、string
とnumber
は継承関係に無いので、それらの型を持った変数同士が互いに代入不可能なのは当然です。
しかし、継承関係にある変数というのは、型が違っても代入可能な場合があります。
オブジェクト指向でポリモーフィズムと呼んでいる仕組みがまさにそれで、親クラス型の変数には、子クラス型の変数を代入することでき、これによって柔軟なコードを書くことができるようになります(下記のコードは仮想の言語で、雰囲気)。
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つの種類が存在します。
- Covariant/Covariance: 親クラス型の変数には、子クラス型の変数を代入できる。
- Contravariant/Contravariance: 子クラス型の変数には、親クラス型の変数を代入できる。
- Bivariant/Bivariance: 継承関係にあるクラス同士であれば、親でも子でも互いに代入できる。
- Invariant/Invariance: 継承関係にあっても、型が異なれば代入はできない。
話を戻すと、TypeScriptでは関数代入時に引数の型チェックの挙動はデフォルトでBivariantです。
ちなみに、関数同士の代入において、下記の条件は必須になります。
- 関数の返り値は、代入先の関数の型の返り値型を全て満たしている。
- 余分にある分には構わない。
- 引数の数は、代入先の関数の引数の数以上である。
- 多い分には構わない。
- オプション引数(
?
つき)でも、個数を満たしていれば代入可能。 - 可変長引数の場合は相手の引数に数に関わらず代入可能。
感覚的には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());
となるからです。
strictFunctionTypes
をtrue
にすることで、この危険な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
この部分。
Cat
はAnimal
クラスの子クラスで、animalArr
、catArr
はそれぞれ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で導入。
詳しく
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
は宣言時もコンストラクタでも初期化されていない。
エラーを回避するためには、
baz: boolean = false;
このように宣言時に初期化するか、もしくは
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
になる場合にエラーにする。
詳しく
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
が定まる場合で、かつそれが意図している参照なのであればアロー関数にするだけで良い。
例えば、
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
してもエラーは消えない)
こういう場合はアロー関数にするだけで良い。
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個目以降の引数が実際のその関数の引数になる。
例えば下記のような、this
をconsole.log
する関数があったとして、
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'.
そこで、
const logThis = function(this: object) {
console.log(this);
};
このように引数でthisを指定すると、console.log(this);
部分のthis
は型が定まる(この場合はobject
)。
この書き方をした場合は、2個目以降の引数が実際の関数の引数になるため、呼び出しは引数を何も取らない形で実行できる。
const logThis = function(this: object) {
console.log(`this`);
};
const obj = { logThis };
obj.logThis();
引数が欲しい場合はこう。
const logThis = function(this: object, label: string) {
console.log(`${label}: ${this}`);
};
const obj = { logThis };
obj.logThis("HOGEHOGE:");
出力される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
の指定値がAMD
・System
・ES2015
のいずれかである場合は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.ts
がimport ... from "./b"
していた場合、
/src/path/to/b.js
-
/src/path/to/main.js
(/src/path/to/package.json
が存在し、その中で"main": "main.js"
という記述があった場合のみ) /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.ts
がimport ... from "b"
していた場合、
/src/path/to/node_modules/b.js
-
/src/path/to/node_modules/b/hoge.js
(package.jsonがあって"main"で指定してたら) /src/path/to/node_modules/b/index.js
/src/path/node_modules/b.js
-
/src/path/node_modules/hoge.js
(package.jsonがあって"main"で指定してたら) /src/path/node_modules/b/index.js
/src/node_modules/b.js
-
/src/node_modules/hoge.js
(package.jsonがあって"main"で指定してたら) /src/node_modules/b/index.js
/node_modules/b.js
-
/node_modules/hoge.js
(package.jsonがあって"main"で指定してたら) /node_modules/b/index.js
な感じ。
さて、やっとtsにおける読み込み順序の話。
tsにおける、relative
基本的にNodeと同じ。ただし、読み込むファイルの拡張子が.js
だけでなく
.ts
.tsx
.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
配下の型定義ファイルも参照の対象になるという点。
優先順位的には
/src/path/to/node_modules/b.ts
/src/path/to/node_modules/b.tsx
/src/path/to/node_modules/b.d.ts
-
/src/path/to/node_modules/b/hoge.d.ts
(package.jsonのtypesで指定されていたら) /src/path/to/node_modules/@types/b.d.ts
/src/path/to/node_modules/b/index.ts
/src/path/to/node_modules/b/index.tsx
/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で良いと思います。
noUncheckedIndexedAccess
"noUncheckedIndexedAccess": true
Include 'undefined' in index signature results.
strictNullChecks
オプションが有効の場合でも、
配列の要素やプロパティへのアクセスは厳密に型チェックできませんが、
undefined
の可能性がある場合にエラーを吐かせるようにできます。
参考: https://tech.techtouch.jp/entry/update-typescript-4.1
forceConsistentCasingInFileNames
"forceConsistentCasingInFileNames": true
Disallow inconsistently-cased references to the same file.
import時にファイルパスの文字列で大文字小文字を区別するかどうかを指定する。
デフォルトfalse。
Windowsは大小区別しないらしい(Winわからん)。
元々区別するOSでtrueになってても弊害は無いので、とりあえずtrueにしておけばOK。
この先工事中...
Next:
- paths
- rootDirs
- typeRoots
- types
- allowSyntheticDefaultImports
- esModuleInterop
- preserveSymlinks
- allowUmdGlobalAccess
- sourceRoot
- mapRoot
- inlineSourceMap
- inlineSources
- experimentalDecorators
- emitDecoratorMetadata