tsconfig.jsonを設定する

Node.jsはそれ自身ではTypeScriptをサポートしているわけではないため、TypeScriptの導入をする時はTypeScriptの設定ファイルであるtsconfig.jsonが必要です。

初めてのtsconfig.json

typescriptpackage.jsondependenciesに入っているプロジェクトで以下を実行してください。

npx tsc --init

typescriptをglobal installしてあれば以下でも可能です。

tsc --init

tsconfig.jsonが作成されます。すでにtsconfig.jsonがある時は上書きされませんのでいったん既存のtsconfig.jsonを別名に変更してから実行など、一度tsconfig.jsonと名のつくファイルが存在しないようにしてください。

公式にあるtsconfig.jsonの説明はこちらです。

全てのオプションの解説をすると余白が足りないので、ここでは用途を抽出して、以下の観点で説明します。

  • targetの決め方

  • フロントエンドとバックエンドで出力設定を変えたほうがよいか

  • Dual Package

target

TypeScriptは最終的にJavaScriptにトランスパイルされます。このオプションはその時に、どのバージョンのJavaScript向けに出力するかといったものです。

JavaScriptも時代とともに進化をして、既存のオブジェクトに新しいメソッドが追加されることがあります。この追加された新しいメソッドが出力するJavaScriptのバージョン以下の時は使えないというわけではなく、TypeScript側で擬似的に再現して補ってくれます。 現在では欠かせない非同期処理のためのPromise, async/awaitも、これらがないバージョンのJavaScriptにトランスパイルをして補い、使えるようにしてくれます。

このような最新バージョンにはある、または現在実装には至っていないが提案中(proposal)である機能を取り入れて使えるようにする物を通称polyfillと言います。

lib

使いたいtargetには使いたい機能がない、でも使いたい。そのような時はlibオプションを指定することで使うことができるようになります。あるtargetを指定すれば、そのtargetで追加されているlibは追加されますのであえて列挙する必要はありません。

targetは何を指定したらいいか

あえて古いコードで動かしているまたは古いNode.jsを使っているといった事情がなければ最新に近い物を指定することは問題ありません。またBabelなどの専用のコンパイラやmodule bundlerに処理を任せたい場合はESNextを指定して、そこからバージョンに合わせたコンパイルを各々にお願いすることになります。

フロントエンドとバックエンド

フロントエンドとバックエンドはモジュールを出力する、他のモジュールを読み込むための作りが異なっています。詳細はimport / export / requireの項をご覧ください。そのため、モジュール解決のためのオプションを分けたほうが無難です。

module

モジュールの仕組みが異なっているライブラリは互換性がある場合もありますが、一般的にはないものと考えてください。

commonjs

バックエンド(サーバーサイド)で使われている解決方法です。作成しているモジュールやパッケージがバックエンドでの動作だけを保証したい場合は最も無難な選択です。

es2015, es2020, esnext

フロントエンドで使われている解決方法です。Node.jsは13.2.0でこのモジュール解決方法をサポートしましたが、現状対応しているパッケージは少なく、使いづらい印象です。

このような違いがあるため、使う場面がバックエンドならcommonjsを、フロントエンドならes2015, es2020, esnextを指定することが望ましいです。

Dual Package

フロントエンドでもバックエンドでもTypeScriptこれ一本!Universal JSという考えがあります。確かにフロントエンドを動的にしたいのであればほぼ避けて通れないJavaScriptと、バックエンドでも使えるようになったJavaScriptで同じコードを使いまわせれば保守の観点でも異なる言語を触る必要がなくなり、統一言語としての価値が大いにあります。

しかしながら前項で述べたように、JavaScriptのモジュール解決の方法がフロントエンドとバックエンドで異なります。この差異のために同じTypeScriptのコードを別々に分けなければいけないかというとそうではありません。ひとつのモジュールをcommonjs, esmoduleの両方に対応した出力をするDual Packageという考えがあります。

Dual Packageことはじめ

名前が仰々しいですが、やることはcommonjs用のJavaScriptとesmodule用のJavaScriptを出力することです。つまり出力するmoduleの分だけtsconfig.jsonを用意します。

プロジェクトはおおよそ以下のような構成になります。

./
├── tsconfig.base.json
├── tsconfig.cjs.json
├── tsconfig.esm.json
└── tsconfig.json
  • tsconfig.base.json

    • 基本となるtsconfig.jsonです

  • tsconfig.cjs.json

    • tsconfig.base.jsonを継承したcommonjs用のtsconfig.jsonです

  • tsconfig.esm.json

    • tsconfig.base.jsonを継承したesmodule用のtsconfig.jsonです

  • tsconfig.json

    • IDEなどはこの名前を優先して探すので、そのためのtsconfig.jsonです

tsconfig.base.jsontsconfig.jsonを分けるかどうかについては好みの範疇です。まとめてしまっても特に問題はありません。

tsconfig.jsonの継承

tsconfig.jsonは他のtsconfig.jsonを継承する機能があります。上記例であればtsconfig.cjs.json, tsconfig.esm.jsonは以下のようにしてtsconfig.base.jsonを継承しているはずです。

// tsconfig.cjs.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "./dist/cjs",
// ...
}
}
// tsconfig.esm.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "esnext",
"outDir": "./dist/esm",
// ...
}
}

outDirはトランスパイルしたjsと、型定義ファイルを出力していれば(後述)それらを出力するディレクトリを変更するオプションです。

このようなtsconfig.xxx.jsonができていれば、あとは以下のようにファイル指定してトランスパイルをするだけです。

tsc -p tsconfig.cjs.json
tsc -p tsconfig.esm.json

Dual Pakcageのためのpackage.json

package.jsonDual Packageのための設定が必要です。

main

package.jsonにあるそのパッケージのエントリポイントとなるファイルを指定する項目です。Dual Packageの時はここにcommonjsのエントリポイントとなるjsファイルを設定します。

module

Dual Packageの時はここにesmoduleのエントリポイントとなるjsファイルを設定します。

types

型定義ファイルのエントリポイントとなるtsファイルを設定します。型定義ファイルを出力するようにしていればcommonjs, esmoduleのどちらのtsconfig.jsonで出力したものでも問題ありません。

package.jsonはこのようになっているでしょう。

{
"name": "YYTS",
"version": "1.0.0",
"license": "Apache-2.0",
"main": "./cjs/index.js",
"module": "./esm/index.js",
"types": "./esm/index.d.ts",
"scripts": {
"build": "yarn build:cjs && yarn build:esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json"
}
}

トランスパイル後のjsのファイルの出力先はあくまでも例です。tsconfig.jsonoutDirを変更すれば出力先を変更できるのでそちらを設定後、package.jsonでエントリポイントとなるjsファイルの設定をしてください。

Tree Shaking

module bundlerの登場により、フロントエンドは今までのような<script>でいろいろなjsファイルを読み込む方式に加えてを全部載せjsにしてしまうという選択肢が増えました。この全部載せjsは開発者としては自分ができる全てをそのまま実行環境であるブラウザに持っていけるので楽になる一方、ひとつのjsファイルの容量が大きくなりすぎるという弱点があります。特にそれがSPA(Signle Page Application)だと問題です。SPAは読み込みが完了してから動作するのでユーザーにしばらく何もない画面を見せることになってしまいます。

この事態を避けるためにmodule bundlerは容量削減のための涙ぐましい努力を続けています。その機能のひとつとして題名のTree Shakingを紹介するとともに、開発者にできるTree Shaking対応パッケージの作り方を紹介します。

Tree Shakingとは

簡潔に申し上げるとTree Shakingとは使われていない関数、クラスを最終的なjsファイルに含めない機能のことです。使っていないのであれば入れる必要はない。というのは至極当然の結論ですが、このTree Shakingができるための条件があります。

  • esmoduleで書かれている

  • 副作用(side effects)のないコードである

以下で詳細を述べます

esmoduleで書かれている

commonjsesmoduleでは外部ファイルの解決方法が異なります。

commonjsrequire()を使用します。require()はファイルのどこでも使用ができますがesmoduleimportはファイルの先頭でやらなければならないという決定的な違いがあります。

require()はある時はこのjsを、それ以外の時はあのjsを、と読み込むファイルをコードで切り替えることができます。つまり、以下のようなことができます。

let police = null;
let firefighter = null;
if (shouldCallPolice()) {
police = require('./police');
} else {
firefighter = require('./firefighter');
}

一方、先述の通りesmoduleはコードに読み込みロジックを混ぜることはできません。

この機能のおかげでmodule bundlerTree Shakingをしにくくなります。そのためesmoduleで書かれている方がmodule bundlerTree Shakingを行うための対応がしやすいという利点があります。

最近ではcommonjsでもTree Shakingができるmodule bundlerも登場しています。

副作用(side effects)のないコードである

ここで言及している副作用とは以下が挙げられます。

  • exportするだけで効果がある

  • プロトタイプ汚染のような、既存のものに対して影響を及ぼす

これらが含まれているかもしれないとmodule bundlerが判断するとTree Shakingの効率が落ちます。

副作用がないことを伝える

module bundlerに制作したパッケージに副作用がないことを伝える方法があります。package.jsonにひとつ加えるだけで完了します。

sideEffects

このプロパティをpackage.jsonに加えて、値をfalseとすればそのパッケージには副作用がないことを伝えられます。

{
"name": "YYTS",
"version": "1.0.0",
"license": "Apache-2.0",
"sideEffects": false,
"main": "./cjs/index.js",
"module": "./esm/index.js",
"types": "./esm/index.d.ts",
"scripts": {
"build": "yarn build:cjs && yarn build:esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json"
}
}

副作用があり、そのファイルが判明している時はそのファイルを指定します。

{
"name": "YYTS",
"version": "1.0.0",
"license": "Apache-2.0",
"sideEffects": [
"./xxx.js",
"./yyy.js"
],
"main": "./cjs/index.js",
"module": "./esm/index.js",
"types": "./esm/index.d.ts",
"scripts": {
"build": "yarn build:cjs && yarn build:esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json"
}
}

パッケージを使う人にもTypeScriptによる型の享受を目指す

パッケージを公開する時は、動作する形で公開するのが前提なのでjsにする必要があります。つまりトランスパイルは必須です。ですがせっかくTypeScriptで作ったのだからパッケージの型情報も提供しましょう。

型定義ファイルも出力する

型定義ファイルを一緒に出力しましょう。そのためにはtsconfig.jsonにあるdeclarationの項目をtrueに変更します。

"declaration": true,
/* Generates corresponding '.d.ts' file. */

このように設定するとトランスパイルで出力したjsファイルと同じディレクトリに同名で拡張子がd.tsのファイルも出力されるようになります。これが型情報のファイルです。なおこの型定義ファイルだけをトランスパイルで出力されたjsと別のディレクトリに出力するためのオプションは存在しません。

変哲もないnumberを持つValue Objectを作ったとします。

class NumericalValueObject {
private value: number;
public constructor(value: number) {
this.value = value;
}
public eq(other: NumericalValueObject): boolean {
return this.value === other.value;
}
public gt(other: NumericalValueObject): boolean {
return this.value > other.value;
}
public gte(other: NumericalValueObject): boolean {
return this.value >= other.value;
}
public lt(other: NumericalValueObject): boolean {
return this.value < other.value;
}
public lte(other: NumericalValueObject): boolean {
return this.value <= other.value;
}
public toString(): string {
return `${this.value}`;
}
}

これをトランスパイルし、型定義を生成するとこのようになっています。

declare class NumericalValueObject {
private value;
constructor(value: number);
eq(other: NumericalValueObject): boolean;
gt(other: NumericalValueObject): boolean;
gte(other: NumericalValueObject): boolean;
lt(other: NumericalValueObject): boolean;
lte(other: NumericalValueObject): boolean;
toString(): string;
}

内容自体は実装部分がないインターフェイスのようなファイルとなっています。

宣言元へのジャンプでのtsファイルを参照できるようにする

IDEを使っている時に有用で、実際のtsのソースコードがどのようにして動作しているのかを閲覧することができるようになります。tsconfig.jsonにあるdeclarationMapの項目をtrueに変更します。

"declarationMap": true,
/* Generates a sourcemap for each corresponding '.d.ts' file. */

このように設定するとトランスパイルで出力したjsファイルと同じディレクトリに同名で拡張子がd.ts.mapのファイルも出力されるようになります。このファイルは元のtsと実際に動作するjsの対応付けをしてくれます。ただしこの設定だけでは不十分で、参照元となる元のtsファイルも一緒にパッケージとして公開する必要があります。

元のtsも公開する

特に設定していなければ元のtsファイルも公開されますが、公開する内容を調整している場合は逆にpackage.jsonfilesプロパティをを変更して元のtsファイルも公開するように変更が必要です。tsconfig.jsondeclarationMapを設定しても元のtsファイルを参照できない時はここで公開する内容を制限していないか確認してください。

{
"name": "YYTS",
"version": "1.0.0",
"license": "Apache-2.0",
"sideEffects": false,
"main": "./cjs/index.js",
"module": "./esm/index.js",
"types": "./esm/index.d.ts",
"files": [
"dist",
"src"
],
"scripts": {
"build": "yarn build:cjs && yarn build:esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json"
}
}

この例はdistにトランスパイルした結果のjs, d.ts, d.ts.mapを、srcに元のtsファイルと想定しています。

JavaScriptのsourceMapも出力する

sourceMapとはAltJSがトランスパイルされたJavaScriptとの行を一致させるものです。これがあることによってデバッグやトレースをしている時に、元のtsファイルの何行目が問題、原因なのかがわかりやすくなります。module bundlerを使用する時はこのオプションを有効にしていないと基本的に何もわかりません。

tsconfig.jsonにあるsourceMapの項目をtrueに変更します。

"sourceMap": true,
/* Generates corresponding '.map' file. */

こちらもトランスパイルで出力したjsファイルと同じディレクトリに同名で拡張子がjs.mapのファイルも出力されるようになります。

より厳密にコーディングする

厳密なコーディングといえばlinterがありますが、TypeScript自身にもより型チェックを厳密にするオプションがあります。以下はtsconfig.jsonの該当する部分を抜粋したものです。

/* 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. */

初期設定ではstrictのみが有効になっています。

以下は各オプションの説明です。

strict

このオプションはTypeScript3.9時点で以下の7個のオプションを全て有効にしていることと同じです。スクラッチから開発するのであれば有効にしておいて差し支えないでしょう。

  • noImplicitAny

  • strictNullChecks

  • strictFunctionTypes

  • strictBindCallApply

  • strictPropertyInitialization

  • noImplicitThis

  • alwaysStrict

この説明にTypeScriptのバージョンが明記されているのは、今後のバージョンでオプションが追加または廃止されることがありうるからです。より安定したオプションを設定したい場合はstrictではなく個々のオプションを有効にしてください。

noImplicitAny

型を明示しない引数のTypeScriptではanyになりますが、これを禁止します。

function increment(i) {
return i + 1;
};

noImplicitAnytrueに設定しこれをトランスパイルしようとすると

Parameter 'i' implicitly has an 'any' type.

となります。これを回避するためには

function increment(i: number): number {
return i + 1;
};

とします。なお戻り値の型は必須ではありません。

strictNullChecks

null, undefinedのチェックが厳密になります。このオプションを入れていると変数に代入する時にnull, undefinedの代入を防げます。

const error: Error = null;

strictNullCheckstrueに設定しこれをトランスパイルしようとすると

Type 'null' is not assignable to type 'Error'.

となります。これを回避するためには

const error: Error | null = null;

とします。

strictFunctionTypes

関数の引数の型チェックが厳格になります。

class RuntimeError extends Error {
private cause?: Error;
public constructor(message: string, cause?: Error) {
super(message);
this.cause = cause;
}
public stacktrace(): string | undefined {
if (typeof this.cause === 'undefined') {
return this.stack;
}
return this.cause.stack;
}
}

Errorを拡張して他のErrorを内包できるようにしたRuntimeErrorがあります。このクラスにはstacktrace()というメソッドがあります。

スタックトレースを出力するメソッドruntimeDump()を作成します。

const runtimeDump = (err: RuntimeError): void => {
console.log(err.stacktrace());
};

もちろん以下は動作します。

runtimeDump(new RuntimeError('runtime error', new Error('error')));

runtimeDump()Error型を引数に受けるType aliasに代入します。

type ErrorDump = (err: Error) => void;
const dump: ErrorDump = runtimeDump;

代入したdump()はもちろん引数にError型を受けることができます。

dump(new Error('error'));

しかしながら、これは落ちてしまいます。

TypeError: err.stacktrace is not a function

これはErrorにはerr.stacktrace()が存在しないことが原因です。

strictFunctionTypestrueに設定しこれをトランスパイルしようとすると

Type '(err: RuntimeError) => void' is not assignable to type 'ErrorDump'.
Types of parameters 'err' and 'err' are incompatible.
Property 'stacktrace' is missing in type 'Error' but required in type 'RuntimeError'.

となります。関数の引数における型は、このオプションを有効にすることで代入時にそのクラスまたはサブクラス以外を禁止します。

strictBindCallApply

function.bind(), function.call(), function.apply()の型チェックが厳密になります。

function stackTrace(error: Error): string | undefined {
return error.stack;
}
stackTrace.call(undefined, null);

strictBindCallApplytrueに設定しこれをトランスパイルしようとすると

Argument of type 'null' is not assignable to parameter of type 'Error'.

となります。これを回避するためにはcall()の第2引数をErrorのインスタンスに変更します。

stackTrace.call(undefined, new ReferenceError());

strictPropertyInitialization

初期化されていない変数定数や、コンストラクタで初期化されていないクラスのプロパティを禁止します。

class User {
public name: string;
public sex: string;
public age: number;
}

strictPropertyInitializationtrueに設定しこれをトランスパイルしようとすると

Property 'name' has no initializer and is not definitely assigned in the constructor.
Property 'sex' has no initializer and is not definitely assigned in the constructor.
Property 'age' has no initializer and is not definitely assigned in the constructor.

となります。これを回避するためにはコンストラクタで初期化するか、初期値を設定します。

class User {
public name: string;
public sex: string;
public age: number;
public constructor(name: string, sex: string, age: number) {
this.name = name;
this.sex = sex;
this.age = age;
}
}
class User {
public name: string = 'John';
public sex: string = 'Doe';
public age: number = 20;
}

このオプションが有効だと、ORMで使うようなプロパティが全てpublicでコンストラクタのないクラスは基本的に作れなくなります。

noImplicitThis

名前付き関数、匿名関数はアロー関数と異なり、実行時にthisが決定されます。そのため、内部でthisを使っているとそれは関数を書いている時点ではanyと同じ扱いになります。このオプションはそれを禁止します。

以下のようなType aliasがあるとします。

type Person = {
name01: string;
name02: string;
name03: string;
name04: string;
name05: string;
name06: string;
name07: string;
name08: string;
name09: string;
name10: string;
name11: string;
name12: string;
name13: string;
name14: string;
name15: string;
name16: string;
name17: string;
name18: string;
name19: string;
name20: string;
intro(): void;
};

そして、アルファベットの先頭1文字を大文字にするcapitalize()nameXXのプロパティを出力するdump()を定義します。

function capitalize(str: string): string {
return `${str[0].toUpperCase()}${str.slice(1)}`;
}
function dump(): string {
const props: string[] = [];
props.push(capitalize(this.name01));
props.push(capitalize(this.name02));
props.push(capitalize(this.name03));
props.push(capitalize(this.name04));
props.push(capitalize(this.name05));
props.push(capitalize(this.name06));
props.push(capitalize(this.name07));
props.push(capitalize(this.name08));
props.push(capitalize(this.name09));
props.push(capitalize(this.name10));
props.push(capitalize(this.name11));
props.push(capitalize(this.name12));
props.push(capitalize(this.name13));
props.push(capitalize(this.name14));
props.push(capitalize(this.name15));
props.push(capitalize(this.name16));
props.push(capitalize(this.name17));
props.push(capitalize(this.name18));
props.push(capitalize(this.name19));
props.push(capitalize(this.name20));
props.push(capitalize(this.name21));
props.push(capitalize(this.name22));
props.push(capitalize(this.name23));
props.push(capitalize(this.name24));
props.push(capitalize(this.name25));
props.push(capitalize(this.name26));
props.push(capitalize(this.name27));
props.push(capitalize(this.name28));
props.push(capitalize(this.name29));
props.push(capitalize(this.name30));
return props.join(' ');
}

これを使いPersonのインスタンスを作成します。

const person: Person = {
name01: 'pablo',
name02: 'diego',
name03: 'josé',
name04: 'francisco',
name05: 'de',
name06: 'paula',
name07: 'juan',
name08: 'nepomuceno',
name09: 'maría',
name10: 'de',
name11: 'los',
name12: 'remedios',
name13: 'cipriano',
name14: 'de',
name15: 'la',
name16: 'santísima',
name17: 'trinidad',
name18: 'ruiz',
name19: 'y',
name20: 'picasso',
intro: dump
};

トランスパイルして実行します。

console.log(person.intro());

落ちます。

TypeError: Cannot read property '0' of undefined

これはプロパティがname20までしかないPersonのオブジェクトリテラルに対してname21 ~ name30のプロパティを取得し、それにcapitalize()を適用しようとしたことが問題です。

noImplicitThistrueに設定しこれをトランスパイルしようとすると大量の以下の警告が表示されます。

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

これを回避するためにはdump()の関数が扱っているthisが何かを指定します。dump()の第1引数をthisとし、その型を書くことでTypeScriptに伝えることができます。

function dump(this: Person): string {
// ...
}

するとTypeScriptは存在しないプロパティについての指摘をするようになります。name21 ~ name30に以下の警告が出るようになります。

Property 'nameXX' does not exist on type 'Person'. Did you mean 'name01'?

この引数のthisについては関数 (Functions)の項に詳細がありますので併せてご参照ください。

alwaysStrict

'use strict'を各ファイルの先頭に付加します。

noUnusedLocals

使用していない変数を禁止します。

function add(n1: string, n2: string): number {
const str: string = 'this is debug message';
// debug(str);
return n1 + n2;
}

noUnusedLocalstrueに設定しこれをトランスパイルしようとすると

'str' is declared but its value is never read.

となります。デバッグをしている時など若干邪魔な時があります。

noUnusedParameters

使用していない引数を禁止します。

function choose(n1: string, n2: string): number {
return n2;
}

noUnusedParameterstrueに設定しこれをトランスパイルしようとすると

'n1' is declared but its value is never read.

となります。これは上記例のように第2引数だけを使用する関数に対しても適用されます。これを回避するためには、使用していない引数を_で始まる名前に変更します。

function choose(_n1: string, n2: string): number {
// ...
}

noImplicitReturns

関数の全ての条件分岐でreturnが行われているかを厳密にチェックします。

function negaposi(num: number): string {
if (num > 0) {
return 'positive';
} else if (num < 0) {
return 'negative';
}
}

noImplicitReturnstrueに設定しこれをトランスパイルしようとすると

Not all code paths return a value.

となります。これを回避するためには戻り値がvoid以外の関数は常に最後にreturnを書くようにするか、場合分けを漏れないように設計するしかありません。

function negaposi(num: number): string {
if (num > 0) {
return 'negative';
} else if (num < 0) {
return 'positive';
}
return '0';
}

noFallthroughCasesInSwitch

fallthroughとはswitchbreakまたはreturnを行わないことを意味します。以下は多くの方がswitchで学習したであろうfallthoughの例です。

function daysOfMonth(month: number): number {
switch (month) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
return 31;
case 2:
return 28;
case 4:
case 6:
case 9:
case 11:
return 30;
default:
throw new Error('INVALID INPUT');
}
}

意図してこのfallthroughを使いこなすよりもバグを産むことの方が遥かに多いため、このオプションはそれを禁止します。

function nextLyric(lyric: string, count: number): string {
switch (lyric) {
case 'we':
return 'will';
case 'will':
if (count === 1) {
return 'we';
}
if (count === 2) {
return 'rock';
}
case 'rock':
return 'you';
default:
throw new Error('YOU ARE A KING!!!');
}
}

noFallthroughCasesInSwitchtrueに設定しこれをトランスパイルしようとすると

Fallthrough case in switch.

となります。これはcase 'will'の時にreturnされない場合がある、つまりfallthroughが発生していることが問題です。

これを回避するためにはcaseでは漏れなくbreakあるいはreturnをするように設計します。

function next(lyric: string, count: number): string {
switch (lyric) {
case 'we':
return 'will';
case 'will':
if (count % 2 === 1) {
return 'we';
}
return 'rock';
case 'rock':
return 'you';
default:
throw new Error('YOU ARE A KING!!!');
}
}

なお、このオプションはcaseに処理がある場合のみbreakあるいはreturnを強制します。この項目で一番初めに紹介した一か月の日数を求める関数daysOfMonth()は、fallthroughであるcaseは全て処理がないため警告は発生しません。