tsconfig.json Deep Dive

基本的なtsconfig.jsonの設定を理解されている前提で話が進みますのでまだの方はすでにある本書のtsconfig.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.jsonのfilesプロパティをを変更して元のtsファイルも公開するように変更が必要です。tsconfig.jsonのdeclarationMapを設定しても元のtsファイルを参照できないときはここで公開する内容を制限していないか確認してください。

{
"name": "YYTS",
"version": "1.0.0",
"license": "CC BY-SA 3.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のファイルも出力されるようになります。

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.jsonとtsconfig.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 Packageのためのpackage.json

package.jsonもDual 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": "CC BY-SA 3.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.jsonのoutDirを変更すれば出力先を変更できるのでそちらを設定後、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はコードに読み込みロジックを混ぜることはできません。

上記例でshouldCallPolice()が常にtrueを返すように作られていたとしてもmodule bundlerはそれを検知できない可能性があります。本来なら必要のないfirefighterを読み込まないという選択を取ることは難しいでしょう。

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

副作用のないコードである

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

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

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

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

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

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

sideEffects

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

{
"name": "YYTS",
"version": "1.0.0",
"license": "CC BY-SA 3.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": "CC BY-SA 3.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"
}
}

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

厳密なコーディングといえばlinterがあります。TypeScript自身にもより型チェックを厳密にするオプションがあります。以下はTypeScript 4.3.2の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. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */

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

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

strict

このオプションはTypeScript4.2時点で次の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 gender: string;
public age: number;
}

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

Property 'name' has no initializer and is not definitely assigned in the constructor.
Property 'gender' 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 gender: string;
public age: number;
public constructor(name: string, gender: string, age: number) {
this.name = name;
this.gender = gender;
this.age = age;
}
}
class User {
public name: string = 'John';
public gender: string = 'Female';
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));
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 ~ name24のプロパティを取得し、それに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 ~ name24に次の警告が出るようになります。

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

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

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で学習したであろうfallthroughの例です。

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 = 1): 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はすべて処理がないため警告は発生しません。

noUncheckedIndexedAccess

インデックス型や配列で宣言されたオブジェクトが持つプロパティへのアクセスが厳密になります。インデックス型についてはタイプエイリアスのページをご参照ください。

type ObjectLiteralLike = {
};
type ArrayObjectLike = {
};
const butterfly: ObjectLiteralLike = {
en: 'Butterfly',
fr: 'Papillon',
it: 'Farfalla',
es: 'Mariposa'
};
const phoneticCodes: ArrayObjectLike = {
0: 'alpha',
1: 'bravo',
2: 'charlie'
};

ObjectLiteralLike, ArrrayObjectLikeは共にstring型のプロパティを持つオブジェクトの型として宣言されています。

const germanName: string = butterfly.de;
const fifth: string = phoneticCodes[4];

これらのオブジェクトのプロパティにアクセスするときは完全な型安全ではありません。上記germanName, fifthはどちらも定義されたオブジェクトには存在しませんがTypeScriptaはこれらをstring型と解釈します。

noUncheckedIndexedAccesstrueに設定しこれらをコンパイルしようとすると

Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.

このように厳密に定義されていないプロパティはundefined型とのユニオン型として解釈されるようになります。

const englishName: string | undefined = butterfly.en;
const first: string | undefined = phoneticCode[0];

ここであるサービスが英語版だけは担保し、他の言語は追々という対応をしたとします。するとそのシステムにある単語や文章を意味する型は次のようになります。

type SystemTerms = {
en: string;
};

このような型を定義するとそのオブジェクトはenプロパティに限りnoUncheckedIndexedAccessの制約を受けません。

const butterfly: SystemTerms = {
en: 'Butterfly',
fr: 'Papillon',
it: 'Farfalla',
es: 'Mariposa'
};
const englishName: string = butterfly.en;
const frenchhName: string | undefined = butterfly.fr;

配列はインデックス記法でアクセスをするとundefined型とのユニオン型と解釈されますがfor-of, array.forEach()はこの制約を受けないため積極的に使用を検討してください。

const phoneticCodes: string[] = ['alpha', 'bravo', 'charlie'];
for (const p of phoneticCodes) {
// ...
}
phoneticCodes.forEach((p: string) => {
// ...
});

noImplicitOverride

サブクラスがスーパークラスのメソッドを拡張したときに override のキーワードをメソッドの前に書くことを強制します。これはスーパークラスの拡張しているメソッドが取り除かれたり、名称が変更されたことを検知することに役立ちます。

例えば、トグルボタン (クリックするとオン、オフを繰り返すボタン) のクラスがあったとすれば、それは次のようになっているでしょう。

class ToggleButton {
protected active: boolean;
public constructor() {
this.active = false;
}
public isActive(): boolean {
return this.active;
}
public enable(): void {
this.active = true;
}
public disable(): void {
this.active = false;
}
public push(): void {
if (this.isActive()) {
this.disable();
// ...
return;
}
this.enable();
// ...
}
}

ここで値のオンオフの切り替えを何回したかを数えられるサブクラス ToggleCountButton を考えます。すると ToggleCountButton は次のようになります。

class ToggleCountButton extends ToggleButton {
private counter: number;
public constructor() {
super();
this.counter = 0;
}
public enable(): void {
this.counter++;
this.active = true;
}
public disable(): void {
this.counter++;
this.active = false;
}
public getCounter(): number {
return this.counter;
}
}

ここでスーパークラスの ToggleButton が「オンオフの切り替えにメソッドはふたつも要らない!セッターで十分だ」と変更したとします。

class ToggleButton {
protected active: boolean;
public isActive(): boolean {
return this.active;
}
public setActive(active: boolean): void {
this.active = active;
}
public push(): void {
if (this.isActive()) {
this.setActive(false);
// ...
return;
}
this.setActive(true);
// ...
}
}

するとサブクラスでオーバーライドしたはずのメソッド enable(), disable() が意味のないメソッドとして残ることになります。

noImplicitOverride はオーバーライドしているメソッドに overrideキーワードをつけることによってスーパークラスに同名のメソッドがないかを確認させます。

noImplicitOverridetrue に設定しオーバーライドしているにもかかわらず override のキーワードを付けずにコンパイルしようとすると

This member must have an 'override' modifier because it overrides a member in the base class 'ToggleButton'.

override キーワードがない旨の指摘を受けることになります。

逆に、オーバーライドしていないメソッドに override キーワードをつけると

This member cannot have an 'override' modifier because it is not declared in the base class 'ToggleButton'.

とスーパークラスのメソッドをオーバーライドしていない旨の指摘を受けることになります。

なお今回の例では仮にnoImplicitOverrideオプションを有効にしていなくても super を使うことによってスーパークラスにメソッドが存在するかどうかを検出することはできます。

class ToggleCountButton extends ToggleButton {
private counter: number;
public constructor() {
super();
this.counter = 0;
}
public enable(): void {
super.enable();
this.counter++;
}
public disable(): void {
super.disable();
this.counter++;
}
public getCounter(): number {
return this.counter;
}

noPropertyAccessFromIndexSignature

noUncheckedIndexedAccess と同様にインデックス型を持つオブジェクトに対する型チェックです。インデックス型に対するアクセスをインデックス記法に強制されます。

ドット記法とインデックス記法についてですが、次のようにあるオブジェクトがあるとしてドット(.)でプロパティアクセスをしているものがドット記法、ブラケット([])でアクセスをしているものがインデックス記法です。

type SystemTerms = {
en: string;
};
const butterfly: SystemTerms = {
en: 'Butterfly',
fr: 'Papillon',
it: 'Farfalla',
es: 'Mariposa'
};
// dot syntax
butterfly.en;
// indexed syntax
butterfly['en'];

SystemTermsnoUncheckedIndexedAccess にて登場した型と同じものでシステムにおける単語、用語のうち英語は担保し他言語の存在は曖昧なものにしています。

console.log(butterfly.fr);
console.log(butterfly['fr']);

noPropertyAccessFromIndexSignaturetrue に設定し次のコードをコンパイルしようとすると

Property 'fr' comes from an index signature, so it must be accessed with ['fr'].
console.log(butterfly.fr);

このようにインデックス型へのドット記法でのアクセスが禁止されます。