TypeScriptのコンパイルオプションはどのように設定されているでしょうか?
大盛無料のノリで全部オンにしてもよいですが、何があるか把握してある程度は把握しておくことも大事です。
さらりと全て見ていきましょう。
※ 執筆時点のTypeScriptのバージョンは5.8です。
※ 間違っていたら指摘してください。
1. noImplicitAny
メソッドの引数の型を必ず指定させます。
下記のFAILの方のメソッドは引数 anything
に型を指定していないため、コンパイルエラーとなります。
// Parameter 'anything' implicitly has an 'any' type.
function example1_FAIL (anything): void {
console.log(anything);
}
// OK
function example1_SUCCESS (anything: unknown): void {
console.log(anything);
}
2. strictNullChecks
型がnullセーフになります。
これがないとstringやnumberといった型にundefinedやnullを代入してもコンパイルエラーになりません。
つまり、下記のFAILのメソッドがまかり通ってしまうことになります。
// Type 'null' is not assignable to type 'number'.
const example2_FAIL: number = null;
// OK
const example2_SUCCESS: number|null = null;
3. strictFunctionTypes
共変(型を広くする)は許可するが、反変(型を狭くする)は許可しません。
例を見てみましょう。
// 反変(型を狭くする)
// Type '(value: number) => void' is not assignable to type '(value: string | number) => any'.
let example3: (value: number|string) => any;
example3 = function (value: number): void {
console.log(value.toFixed());
};
// 上記の場合、以下が実行されるとランタイムエラーを発生させてしまう
example3('文字列');
// 共変(型を広くする)はOK
example3 = function (value: number|string|null|undefined): void {
// value.toFixed() を実行したい場合はきちんと型を絞った上で実装しないとエラー
console.log(value);
};
1つ目のメソッドでは、引数が number|string
型のメソッドに対して、引数が number
型のメソッドを代入しています。
この状態は危険であり、メソッドの内部では引数が number
型を前提とした処理が記載できてしまいます。一方で実際の引数の型は number|string
型 なので、 string
型を受け入れることが可能です。
例の通り number
型を前提とした処理が記載されていた場合、実行時エラーになってしまいます。
このことを防ぐのがstrictFunctionTypesコンパイルオプションになります。
逆に2つ目のメソッドは型を広くして代入しています。
これはエラーにはなりません。例えばこの中で value.toFixed()
を実行したければ、型を絞ってから記載しないとコンパイルエラーとなるからです。
なので共変(型を広くする)は安全ですが、反変(型を狭くする)は危険なので許可しないという内容になります。
4. strictBindCallApply
bind call apply を使用した時に引数の型チェックを行うようになります。
function example4 (args: string): void {
console.log(`${args.toUpperCase()}`);
}
// Argument of type 'number' is not assignable to parameter of type 'string'.
example4.bind(null, 100);
example4.call(null, 200);
example4.apply(null, 300);
そもそも bind
apply
call
を使用したことはあるでしょうか?これらを使用すると this
を使用する少し変わったメソッドの呼び出しが可能になります。
デフォルトだとこられの呼び出し時の引数が any
になってしまうので、それらに適切に型を付けるオプションとなります。
表現を変えると、このオプションがOFFだと例のコードはコンパイルエラーになりません。(もちろん実行時エラーになります)
5. strictPropertyInitialization
クラス内で初期化されていないプロパティを禁止します。
// Property 'property1' has no initializer and is not definitely assigned in the constructor.
// Property 'property2' has no initializer and is not definitely assigned in the constructor.
// Property 'property3' has no initializer and is not definitely assigned in the constructor.
class example5_FAIL {
property1: string;
property2: string;
property3: string;
constructor () { }
}
// OK
class example5_SUCCESS {
property1: string = 'OK';
property2: string;
property3: string|undefined;
constructor () {
this.property2 = 'OK';
}
}
そもそもTypeScriptでクラスを使う機会自体が少ないかもしれませんが、覚えておいて損はないでしょう。
余談ですが、コンストラクタ内で別のメソッドを呼び出し、そのメソッド内で初期化をしていても検知できずにコンパイルエラーとなってしまった記憶があります。
少し不便に感じますが、コンストラクタ内からそのメソッドを呼び出して初期化するという暗黙的な依存が生まれるとも解釈できるので、仕方なさそうです。
6. strictBuiltinIteratorReturn
これは最近追加されたオプションで、イテレータをnext()で辿った時の型チェックを厳密にします。
どういうことでしょうか。
const example6: Iterator<string, BuiltinIteratorReturn> = ['one', 'two', 'three'].values();
// undefinedを踏んでしまうのでtoUpperCase()のところでランタイムエラーを起こしてしまう
// 'example6_FAIL.value' is possibly 'undefined'.
const example6_FAIL = example6.next();
console.log(example6_FAIL.value.toUpperCase());
// OK
const example6_SUCCESS = example6.next();
if (example6_SUCCESS.done) {
// undefined 確定
console.log(example6_SUCCESS.value);
} else {
// string 確定
console.log(example6_SUCCESS.value.toUpperCase());
}
example6
は string
型のIterableな値ですが、 next()
でループをすると undefined
を踏んでしまう可能性があります。
これは next()
の仕様によるものです。
undefined
を踏まずに安全にループをするには、 SUCCESSの方のメソッドのように example6_SUCCESS.done
による分岐で参照が undefined
でないか検証する必要があります。
7. noImplicitThis
関数内でthisを使う場合、明示的に型の指定を強制します。
const exmaple7 = {
name: 'name',
greetFail: exmaple7_FAIL,
greetSuccess: exmaple7_SUCCESS
}
// 'this' implicitly has type 'any' because it does not have a type annotation.
function exmaple7_FAIL () {
console.log(`${this.name}`);
}
type example7 = {
name: string;
}
// 引数でthisの型を明示してあげればOK
function exmaple7_SUCCESS (this: example7): void {
console.log(`${this.name}`);
}
FAILの方のメソッドの this.name
は何を指しているのでしょうか?
exmaple7
を指しているのだと思われますが、これだとエラー以前に分かりにくいです。
SUCCESSの方のように、引数に型を明示した this
を渡すことを強制させるオプションです。
8. useUnknownInCatchVariables
try catch での例外補足時、catch節に渡る値をunknown型とします。
// 'example8_FAIL' is of type 'unknown'.
try {
throw 'ERROR';
} catch (example8_FAIL) {
console.log(example8_FAIL.toUpperCase())
}
例では example8_FAIL
が string
型の前提で処理が書かれていますが、このオプションを有効にすると型エラーとなります。
string
型が期待されているのであれば、型を条件分岐で絞ったうえで処理を記載する必要があります。
余談ですが console.log()
するだけなら unknown
型でも問題はありません。例えば console.log(example8_FAIL)
とするのであれば何ら問題はありません。
9. alwaysStrict
グローバルにstrictモードを有効にします。
strictモードを有効にすると、JavaScriptに関する危険寄りのコーディングができなくなります。
// 例えば var let const なしでの暗黙的なグローバル変数の作成をエラーにする
// Cannot find name 'example9_FAIL'. Did you mean 'example1_FAIL'?
example9_FAIL = undefined;
// OK
const example9_SUCCESS = undefined;
JavaScriptでは const や let を宣言せず変数へ代入した場合、その変数はグローバル変数になるという特徴を持っています。
これでは初学者がうっかりconst等を宣言しないまま代入してしまい、知らずのうちにグローバル変数が生まれてしまう事故が発生しうります。
これらのようなJavaScriptが持っている「落とし穴」をコンパイルエラーにしてくれるのがこのオプションです。
10. noUnusedLocals
未使用のローカル変数をエラーにします。
function example10_FAIL (): void {
// 'unused' is declared but its value is never read.
const unused = 'unused';
console.log('unused');
}
function example10_SUCCESS (): void {
console.log('OK');
}
11. noUnusedParameters
未使用の引数があった場合エラーを吐きます。
// 'args2' is declared but its value is never read.
function example11_FAIL (args1: string, args2: string, args3: string): void {
console.log(`${args1} ${args3}`);
}
// 使わないものは _ を先頭につけて表現すればOK
// ただ eslint ではエラー出ちゃうので注意
function example11_SUCCESS (args1: string, _args2: string, args3: string): void {
console.log(`${args1} ${args3}`);
}
例文内にも記載していますが、引数名を _
から開始すればエラーにならないようになります。
ライブラリを使用しており、コールバック関数で2つ目の引数は使用したいが1つ目は使わない、という時に便利です。
12. exactOptionalPropertyTypes
オプショナルプロパティに対する、明示的な undefined
の代入を禁止します。
type example12 = {
property1: string;
property2?: string;
}
// Type '{ property1: string; property2: undefined; }' is not assignable to type 'example12' with ...
const example12_FAIL: example12 = {
property1: 'OK',
property2: undefined
}
// OK
const example12_OK: example12 = {
property1: 'OK'
}
明示的に代入したい場合は、オプショナルプロパティではなく undefined
とのユニオン型にする必要があります。
13. noImplicitReturns
戻り値の型を厳密に評価します。
// Function lacks ending return statement and return type does not include 'undefined'.
function example13_FAIL (args1: number): string {
if (args1 > 0) {
return 'OK';
}
}
// OK
function example13_SUCCESS (args1: number): string {
if (args1 > 0) {
return 'OK';
}
return '';
}
FAILの方の関数は条件によっては return する値がありません。
SUCCESSのように全ての分岐で string
型が返るようにする、あるいは戻り値の型を string|void
とするべきです。
14. noFallthroughCasesInSwitch
switch文でbreak, returnが適切に使用されているか調べます。
function example14_FAIL (args1: number): void {
switch (args1) {
case 1: // Fallthrough case in switch.
console.log(args1);
case 2: // Fallthrough case in switch.
console.log(args1);
case 3: // Fallthrough case in switch.
console.log(args1);
default:
console.log(args1);
}
}
// OK
function example14_SUCCESS (args1: number): void {
switch (args1) {
case 1:
console.log(args1);
break;
case 2:
console.log(args1);
break;
case 3:
console.log(args1);
return;
default:
console.log(args1);
}
}
case 毎に break や return を設けなさいということです。
15. noUncheckedIndexedAccess
配列アクセス時の型はundefinedとのユニオン型とします。
const example15: number[] = [1, 2, 3];
// Type 'number | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'.
const example15_FAIL: number = example15[1];
// Object is possibly 'undefined'.
example15[1] += 1;
const example15_SUCCESS: number = example15[1] ?? 0;
example15[1] = example15[1] ?? 0 + 1;
JavaScriptでは存在しないインデックスを指定して配列にアクセスしても、エラーにはならず undefined
を返します。
つまり配列へのアクセスは常に undefined
を取得する可能性があるということです。
このオプションは賛否両論あるように思われます。配列へのアクセスをしている部分全てで undefined
とのユニオン型となるので、厳密に扱いたい場合は undefined
のチェックが至る所で発生することになります。
固定で要素を持っている配列があり、アクセスしても絶対に値があるのにもかかわらず undefined
が付きまとい辟易することもあるでしょう。
儀式的な undefined
チェックが至る所に発生し、コードの可読性も悪くなってしまいます。
個人的に思うのは、そういう場合でもコンパイルオプションをオフにするのではなく別の選択肢を考えてほしいと思っています。
例えば確実に値があると分かっているのであれば、その部分だけは Non-null assertion operator を使用すれば解決します。
const example15: number[] = [1, 2, 3];
const example15_FAIL: number = example15[1]!;
代入時に !
をつけてコンパイラに null
や undefined
の可能性がないことを教えています。
これを使うのだったらこのオプションを有効にする必要はないのでは?と考える方もいらっしゃるかもしれません。
アプローチは全て0か100ではないと思っています。つまり確実に値があることが自明である場所のみ !
を使用し、そうでない場所は undefined
とのチェックを挟めばいいわけです。
コンパイラオプションの有効か無効かの2択ではなく、もっとたくさんの選択肢があってもいいのではないかと個人的に思っています。
16. noImplicitOverride
オーバライドの明示的な宣言を強制します。
class Example15_Parent {
public example15_FAIL (): void {
console.log('parent');
}
public example15_SUCCESS (): void {
console.log('parent');
}
}
class Example15_Child extends Example15_Parent {
// This member must have an 'override' modifier because it overrides a member in the base class 'Example15_Parent'.
public example15_FAIL (): void {
console.log('child');
}
// OK
public override example15_SUCCESS (): void {
console.log('child');
}
}
17. noPropertyAccessFromIndexSignature
オブジェクトのプロパティが存在するか不明瞭な場合、プロパティアクセスをインデックス記法に強制します。
type Example17 = {
[key: string]: string;
};
const example17: Example17 = {
FAIL: 'NG',
SUCCESS: 'OK',
}
// Property 'FAIL' comes from an index signature, so it must be accessed with ['FAIL'].
console.log(example17.FAIL);
console.log(example17['SUCCESS']);
インデックス記法とは []
を使ったアクセスのことです。
他にもドット .
を使ってもプロパティにアクセスできますが、プロパティが存在するか不明瞭な場合はドット表記のアクセスを禁止するというものです。
18. allowUnusedLabels
使用されていないラベルにエラーを出します。
このオプションはfalseすることで有効になります。
example18_SUCCESS: for (let i = 0; i < 5; i++) {
if (i === 1) {
continue example18_SUCCESS;
}
if (i === 2) {
// Unused label.
example18_FAIL: true;
}
console.log(i);
}
そもそもJavaScriptでラベルが使われているのをあまり見たことがないです。
19. allowUnreachableCode
未到達コードにエラーを出します。
このオプションはfalseすることで有効になります。
function example19 (args1: number): number {
if (args1 > 0) {
return args1 + 1;
} else {
return args1 + 2;
}
// Unreachable code detected.
return args1 + 3;
}
終わりに
後半はどちらかというとlint寄りのものが多かったと思います。
コンパイラオプションはこれからも追加される可能性があるので、日々チェックをしていけたらと思います。
これで安心してオールオンにできますね。