複雑な Web アプリケーションを構築している場合、プログラミング言語として TypeScript を選択することになるでしょう。 TypeScript は、強力な型システムと静的分析機能で広く愛されており、コードが堅牢でエラーがないことを保証するための強力なツールとなっています。
また、コード エディターとの統合により開発プロセスが高速化され、開発者がより効率的にコードを操作し、より正確なヒントやオートコンプリートを取得できるようになり、大量のコードの安全なリファクタリングが可能になります。
コンパイラーは TypeScript の中心であり、型の正確性をチェックし、TypeScript コードを JavaScript に変換します。ただし、TypeScript の機能を最大限に活用するには、コンパイラーを正しく構成することが重要です。
各 TypeScript プロジェクトには、コンパイラーのすべての構成オプションを保持する 1 つ以上のtsconfig.json
ファイルがあります。
tsconfig の構成は、TypeScript プロジェクトで最適なタイプ セーフティと開発者エクスペリエンスを実現するための重要な手順です。関連するすべての重要な要素を時間をかけて慎重に検討することで、開発プロセスをスピードアップし、コードが堅牢でエラーがないことを保証できます。
tsconfig のデフォルト構成により、開発者は TypeScript の利点の大部分を逃す可能性があります。これは、多くの強力な型チェック機能が有効にならないためです。 「デフォルト」構成とは、型チェック コンパイラ オプションが設定されていない構成を意味します。
例えば:
{ "compilerOptions": { "target": "esnext", "module": "esnext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, }, "include": ["src"] }
いくつかの主要な構成オプションがないと、主に 2 つの理由でコードの品質が低下する可能性があります。まず、TypeScript のコンパイラは、さまざまな場合にnull
およびundefined
型を誤って処理する可能性があります。
次に、 any
型がコードベースに制御不能に出現する可能性があり、この型に関する型チェックが無効になる可能性があります。
幸いなことに、これらの問題は、構成内のいくつかのオプションを調整することで簡単に修正できます。
{ "compilerOptions": { "strict": true } }
Strict モードは、幅広い型チェック動作を有効にすることで、プログラムの正確性をより強力に保証する必須の構成オプションです。
tsconfig ファイルで厳密モードを有効にすることは、最大限の型安全性とより良い開発者エクスペリエンスを実現するための重要なステップです。
tsconfig の構成には少し手間がかかりますが、プロジェクトの品質を向上させるのに大いに役立ちます。
strict
コンパイラ オプションは、特にnoImplicitAny
、 strictNullChecks
、 strictFunctionTypes
の strict モード ファミリ オプションをすべて有効にします。
これらのオプションは個別に設定することもできますが、いずれかをオフにすることはお勧めできません。その理由を例を見てみましょう。
{ "compilerOptions": { "noImplicitAny": true } }
any
型は静的型システムの危険な抜け穴であり、これを使用するとすべての型チェック ルールが無効になります。その結果、バグが見逃されたり、コード エディターのヒントが適切に機能しなくなったりするなど、TypeScript のすべての利点が失われます。
any
使用しても問題がないのは、極端な場合またはプロトタイピングが必要な場合のみです。最善の努力にもかかわらず、 any
型が暗黙的にコードベースに侵入する可能性があります。
デフォルトでは、コンパイラは、 any
ベースにエラーが発生しても、多くのエラーを許容します。具体的には、TypeScript を使用すると、型が自動的に推論できない場合でも、変数の型を指定しなくても済みます。
問題は、関数の引数などに変数の型を指定することをうっかり忘れてしまう可能性があることです。 TypeScript はエラーを表示する代わりに、変数の型をany
として自動的に推測します。
function parse(str) { // ^? any return str.split(''); } // TypeError: str.split is not a function const res1 = parse(42); const res2 = parse('hello'); // ^? any
noImplicitAny
コンパイラ オプションを有効にすると、コンパイラは変数の型がany
として自動的に推論されるすべての場所を強調表示します。この例では、TypeScript は関数の引数の型を指定するように求めます。
function parse(str) { // ^ Error: Parameter 'str' implicitly has an 'any' type. return str.split(''); }
型を指定すると、TypeScript は文字列パラメーターに数値を渡す際のエラーをすぐに検出します。変数res2
に格納される関数の戻り値も正しい型になります。
function parse(str: string) { return str.split(''); } const res1 = parse(42); // ^ Error: Argument of type 'number' is not // assignable to parameter of type 'string' const res2 = parse('hello'); // ^? string[]
{ "compilerOptions": { "useUnknownInCatchVariables": true } }
useUnknownInCatchVariables
を構成すると、try-catch ブロックで例外を安全に処理できるようになります。デフォルトでは、TypeScript は catch ブロック内のエラー タイプがany
であると想定するため、エラーに対して何でもできるようになります。
たとえば、キャッチしたエラーをそのまま、 Error
のインスタンスを受け入れるログ関数に渡すことができます。
function logError(err: Error) { // ... } try { return JSON.parse(userInput); } catch (err) { // ^? any logError(err); }
ただし、実際には、エラーのタイプについての保証はなく、実行時にエラーが発生したときにのみ、その真のタイプを判断できます。ロギング関数がError
ではないものを受信すると、実行時エラーが発生します。
したがって、 useUnknownInCatchVariables
オプションは、エラーの種類をany
」からunknown
に切り替えて、何かを行う前にエラーの種類を確認するように促します。
try { return JSON.parse(userInput); } catch (err) { // ^? unknown // Now we need to check the type of the value if (err instanceof Error) { logError(err); } else { logError(new Error('Unknown Error')); } }
TypeScript は、 logError
関数に渡す前にerr
変数の型を確認するように求めるプロンプトを表示するため、より正確で安全なコードが得られます。残念ながら、このオプションは、 promise.catch()
関数またはコールバック関数での入力エラーには役に立ちません。
ただし、 any
ような場合の対処方法については、次の記事で説明します。
{ "compilerOptions": { "strictBindCallApply": true } }
別のオプションは、 call
およびapply
を介したany
内呼び出しの外観を修正します。これは最初の 2 つのケースほど一般的ではありませんが、考慮することが重要です。デフォルトでは、TypeScript はそのような構造の型をまったくチェックしません。
たとえば、関数の引数として何でも渡すことができ、最終的には常にany
型を受け取ることになります。
function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? any const n2 = parse.call(undefined, false); // ^? any
strictBindCallApply
オプションを有効にすると、TypeScript がよりスマートになるため、戻り値の型がnumber
として正しく推論されます。また、間違った型の引数を渡そうとすると、TypeScript はエラーを指摘します。
function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? number const n2 = parse.call(undefined, false); // ^ Argument of type 'boolean' is not // assignable to parameter of type 'string'.
{ "compilerOptions": { "noImplicitThis": true } }
プロジェクト内でany
出現を防ぐのに役立つ次のオプションは、関数呼び出しでの実行コンテキストの処理を修正します。 JavaScript の動的性質により、関数内のコンテキストのタイプを静的に決定することが困難になります。
デフォルトでは、TypeScript はそのような場合にコンテキストにany
型を使用し、警告を出しません。
class Person { private name: string; constructor(name: string) { this.name = name; } getName() { return function () { return this.name; // ^ 'this' implicitly has type 'any' because // it does not have a type annotation. }; } }
noImplicitThis
コンパイラ オプションを有効にすると、関数のコンテキストの種類を明示的に指定するように求められます。このようにして、上記の例では、 Person
クラスのname
フィールドではなく関数コンテキストにアクセスした際のエラーを捕捉できます。
{ "compilerOptions": { "strictNullChecks": true } }
次に、 strict
モードに含まれるいくつかのオプションでは、コードベースにany
型が表示されません。ただし、TS コンパイラーの動作がより厳密になり、開発中により多くのエラーが見つかる可能性があります。
最初のこのようなオプションは、TypeScript でのnull
とundefined
の処理を修正します。デフォルトでは、TypeScript はnull
とundefined
どの型に対しても有効な値であると想定するため、予期しない実行時エラーが発生する可能性があります。
strictNullChecks
コンパイラ オプションを有効にすると、開発者はnull
とundefined
発生する可能性があるケースを明示的に処理する必要があります。
たとえば、次のコードを考えてみましょう。
const users = [ { name: 'Oby', age: 12 }, { name: 'Heera', age: 32 }, ]; const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } console.log(loggedInUser.age); // ^ TypeError: Cannot read properties of undefined
このコードはエラーなしでコンパイルされますが、「Max」という名前のユーザーがシステムに存在しない場合、実行時エラーがスローされる可能性があり、 users.find()
はundefined
返します。これを防ぐには、 strictNullChecks
コンパイラ オプションを有効にします。
TypeScript では、 users.find()
によって返されるnull
またはundefined
可能性を明示的に処理する必要があります。
const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } | undefined if (loggedInUser) { console.log(loggedInUser.age); }
null
とundefiined
の可能性を明示的に処理することで、実行時エラーを回避し、コードをより堅牢でエラーのないものにすることができます。
{ "compilerOptions": { "strictFunctionTypes": true } }
strictFunctionTypes
を有効にすると、TypeScript のコンパイラがよりインテリジェントになります。バージョン 2.6 より前の TypeScript では、関数の引数の反変性がチェックされませんでした。これにより、関数が間違った型の引数で呼び出された場合、実行時エラーが発生します。
たとえば、関数型が文字列と数値の両方を処理できる場合でも、文字列のみを処理できる関数をその型に割り当てることができます。その関数に数値を渡すことはできますが、実行時エラーが発生します。
function greet(x: string) { console.log("Hello, " + x.toLowerCase()); } type StringOrNumberFn = (y: string | number) => void; // Incorrect Assignment const func: StringOrNumberFn = greet; // TypeError: x.toLowerCase is not a function func(10);
幸いなことに、 strictFunctionTypes
オプションを有効にするとこの動作が修正され、コンパイラはコンパイル時にこれらのエラーを検出して、関数の型の非互換性に関する詳細なメッセージを表示できるようになります。
const func: StringOrNumberFn = greet; // ^ Type '(x: string) => void' is not assignable to type 'StringOrNumberFn'. // Types of parameters 'x' and 'y' are incompatible. // Type 'string | number' is not assignable to type 'string'. // Type 'number' is not assignable to type 'string'.
{ "compilerOptions": { "strictPropertyInitialization": true } }
最後に重要なことですが、 strictPropertyInitialization
オプションを使用すると、値としてundefined
含まない型の必須クラス プロパティの初期化をチェックできます。
たとえば、次のコードでは、開発者はemail
プロパティを初期化するのを忘れています。デフォルトでは、TypeScript はこのエラーを検出しないため、実行時に問題が発生する可能性があります。
class UserAccount { name: string; email: string; constructor(name: string) { this.name = name; // Forgot to assign a value to this.email } }
ただし、 strictPropertyInitialization
オプションが有効になっている場合、TypeScript はこの問題を強調表示します。
email: string; // ^ Error: Property 'email' has no initializer and // is not definitely assigned in the constructor.
{ "compilerOptions": { "noUncheckedIndexedAccess": true } }
noUncheckedIndexedAccess
オプションはstrict
モードの一部ではありませんが、プロジェクトのコード品質の向上に役立つ別のオプションです。これにより、インデックス アクセス式の戻り値の型がnull
またはundefined
であるかどうかをチェックできるようになり、実行時エラーを防ぐことができます。
キャッシュされた値を保存するためのオブジェクトがある次の例を考えてみましょう。次に、キーの 1 つの値を取得します。もちろん、目的のキーの値が実際にキャッシュに存在するという保証はありません。
デフォルトでは、TypeScript は値が存在し、その型がstring
であると想定します。これにより、実行時エラーが発生する可能性があります。
const cache: Record<string, string> = {}; const value = cache['key']; // ^? string console.log(value.toUpperCase()); // ^ TypeError: Cannot read properties of undefined
TypeScript でnoUncheckedIndexedAccess
オプションを有効にするには、 undefined
戻り値の型についてインデックス アクセス式をチェックする必要があります。これにより、実行時エラーを回避できます。これは、配列内の要素へのアクセスにも当てはまります。
const cache: Record<string, string> = {}; const value = cache['key']; // ^? string | undefined if (value) { console.log(value.toUpperCase()); }
説明したオプションに基づいて、タイプ セーフを最適化するために、プロジェクトのtsconfig.json
ファイルでstrict
オプションとnoUncheckedIndexedAccess
オプションを有効にすることを強くお勧めします。
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, } }
strict
オプションをすでに有効にしている場合は、 strict: true
オプションの重複を避けるために、次のオプションを削除することを検討してください。
noImplicitAny
useUnknownInCatchVariables
strictBindCallApply
noImplicitThis
strictFunctionTypes
strictNullChecks
strictPropertyInitialization
また、型システムを弱めたり、実行時エラーを引き起こす可能性がある次のオプションを削除することをお勧めします。
keyofStringsOnly
noStrictGenericChecks
suppressImplicitAnyIndexErrors
suppressExcessPropertyErrors
これらのオプションを慎重に検討して構成することで、TypeScript プロジェクトで最適なタイプ セーフとより良い開発者エクスペリエンスを実現できます。
TypeScript は、コンパイラと型システムを絶えず改良しながら、長い進化を遂げてきました。ただし、下位互換性を維持するために、TypeScript の構成はより複雑になり、型チェックの品質に大きな影響を与える可能性のある多くのオプションが追加されました。
これらのオプションを慎重に検討して構成することで、TypeScript プロジェクトで最適なタイプ セーフとより良い開発者エクスペリエンスを実現できます。どのオプションを有効にし、プロジェクト構成から削除するかを理解しておくことが重要です。
特定のオプションを無効にした場合の結果を理解すると、それぞれのオプションについて情報に基づいた決定を下せるようになります。
厳密に型指定すると結果が生じる可能性があることに留意することが重要です。 JavaScript の動的な性質に効果的に対処するには、単に変数の後に「数値」または「文字列」を指定するだけでなく、TypeScript についてよく理解する必要があります。
発生する型関連の問題をより効果的に解決するには、より複雑な構造と、ライブラリとツールの TypeScript ファーストのエコシステムに精通する必要があります。
そのため、コードを書くのにもう少し労力が必要になるかもしれませんが、私の経験に基づくと、長期的なプロジェクトではこの労力は価値があります。
この記事から何か新しいことを学んでいただければ幸いです。これはシリーズの最初の部分です。次の記事では、TypeScript の標準ライブラリの型を改善することで、より優れた型安全性とコード品質を実現する方法について説明します。ご期待ください。読んでいただきありがとうございます!