TypeScript は、JavaScript 上に構築された厳密に型指定されたプログラミング言語であり、あらゆる規模で優れたツールを提供すると主張しています。ただし、 TypeScriptにはany
型が含まれており、多くの場合、暗黙的にコードベースに侵入し、TypeScript の利点の多くが失われる可能性があります。
この記事では、TypeScript プロジェクトでany
型を制御する方法について説明します。 TypeScript のパワーを解き放ち、究極のタイプ セーフを実現し、コードの品質を向上させる準備をしましょう。
TypeScript は、開発者のエクスペリエンスと生産性を向上させるためのさまざまな追加ツールを提供します。
ただし、コードベースでany
型を使用し始めるとすぐに、上記の利点がすべて失われます。 any
型は型システムの危険な抜け穴であり、これを使用すると、すべての型チェック機能と、型チェックに依存するすべてのツールが無効になります。その結果、バグが見逃されたり、コード エディターの有用性が低下したりするなど、TypeScript の利点がすべて失われます。
たとえば、次の例を考えてみましょう。
function parse(data: any) { return data.split(''); } // Case 1 const res1 = parse(42); // ^ TypeError: data.split is not a function // Case 2 const res2 = parse('hello'); // ^ any
上記のコードでは次のようになります。
parse
関数内のオートコンプリートを見逃してしまいます。 data.
エディターでは、 data
に使用できるメソッドについて正しい提案が表示されません。TypeError: data.split is not a function
。 any
型チェックを無効にするため、TypeScript はエラーを強調表示できません。res2
変数もany
型を持ちます。これは、 any
を 1 回使用すると、コードベースの大部分に連鎖的な影響を与える可能性があることを意味します。
any
使用しても問題がないのは、極端な場合またはプロトタイピングが必要な場合のみです。一般に、TypeScript を最大限に活用するには、 any
使用も避けることをお勧めします。
any
明示的に記述することが唯一の選択肢ではないため、コードベース内のany
型のソースを認識することが重要です。 any
型の使用を避けるための最善の努力にもかかわらず、any 型が暗黙的にコードベースに侵入する可能性があります。
コードベースには、 any
タイプの主なソースが 4 つあります。
any
の明示的な使用。
最初の 2 つの点については 、「tsconfig の主な考慮事項」と「標準ライブラリの型の改善」に関する記事をすでに書きました。プロジェクトのタイプ セーフティを改善したい場合は、ぜひチェックしてください。
今回は、コードベース内のany
型の外観を制御するための自動ツールに焦点を当てます。
ESLint は、Web 開発者がベスト プラクティスとコードのフォーマットを確保するために使用する一般的な静的分析ツールです。これを使用して、コーディング スタイルを強制し、特定のガイドラインに準拠していないコードを見つけることができます。
ESLint は、 typesctipt-eslintプラグインのおかげで、TypeScript プロジェクトでも使用できます。おそらく、このプラグインはプロジェクトにすでにインストールされています。ただし、そうでない場合は、公式のスタートガイドに従うことができます。
typescript-eslint
の最も一般的な構成は次のとおりです。
module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', root: true, };
この構成により、 eslint
構文レベルで TypeScript を理解できるようになり、コード内で手動で記述された型に適用される単純な eslint ルールを作成できるようになります。たとえば、 any
明示的な使用を禁止できます。
recommended
プリセットには、コードの正確性を向上させることを目的とした、慎重に選択された ESLint ルールのセットが含まれています。プリセット全体を使用することをお勧めしますが、この記事では、 no-explicit-any
ルールのみに焦点を当てます。
TypeScript の厳密モードでは、暗黙的なany
の使用は禁止されていますが、明示的にany
使用されることは妨げられません。 no-explicit-any
ルールは、コードベース内の任意のany
に手動で書き込むことを禁止するのに役立ちます。
// ❌ Incorrect function loadPokemons(): any {} // ✅ Correct function loadPokemons(): unknown {} // ❌ Incorrect function parsePokemons(data: Response<any>): Array<Pokemon> {} // ✅ Correct function parsePokemons(data: Response<unknown>): Array<Pokemon> {} // ❌ Incorrect function reverse<T extends Array<any>>(array: T): T {} // ✅ Correct function reverse<T extends Array<unknown>>(array: T): T {}
このルールの主な目的は、チーム全体でany
の使用を禁止することです。これは、プロジェクト内でany
使用は推奨されないというチームの合意を強化する手段です。
any
を 1 回使用しただけでも、型推論によりコードベースの重要な部分に連鎖的な影響を与える可能性があるため、これは重要な目標です。しかし、これは究極の型安全性の達成にはまだ程遠いです。
明示的に使用されるany
を扱いましたが、npm パッケージや TypeScript の標準ライブラリなど、プロジェクトの依存関係内には依然として多くの暗黙的なany
存在します。
次のコードを考えてみましょう。これはどのプロジェクトでも見られる可能性があります。
const response = await fetch('https://pokeapi.co/api/v2/pokemon'); const pokemons = await response.json(); // ^? any const settings = JSON.parse(localStorage.getItem('user-settings')); // ^? any
変数pokemons
とsettings
両方に暗黙的にany
タイプが与えられました。この場合、 no-explicit-any
も TypeScript の strict モードも警告しません。まだ。
これは、 response.json()
とJSON.parse()
の型が TypeScript の標準ライブラリから取得されており、これらのメソッドには明示的なany
注釈が付いているために発生します。変数に対してより適切な型を手動で指定することはできますが、標準ライブラリにはany
の型が 1,200 近く存在します。標準ライブラリからコードベースに侵入できるany
のケースを覚えておくことはほぼ不可能です。
外部依存関係についても同様です。 npm には型指定が不十分なライブラリが多数あり、そのほとんどは依然として JavaScript で書かれています。結果として、このようなライブラリを使用すると、コードベース内に暗黙的なany
が大量に発生する可能性があります。
一般に、コードに侵入any
方法はまだたくさんあります。
理想的には、何らかの理由でany
型を受け取った変数についてコンパイラにエラーを報告させるような設定を TypeScript に含めたいと考えています。残念ながら、そのような設定は現在存在せず、追加される予定もありません。
この動作は、 typescript-eslint
プラグインの型チェック モードを使用することで実現できます。このモードは TypeScript と連携して動作し、TypeScript コンパイラから ESLint ルールに完全な型情報を提供します。この情報を使用すると、基本的に TypeScript の型チェック機能を拡張する、より複雑な ESLint ルールを作成できます。たとえば、ルールは、 any
取得方法に関係なく、 any
型のすべての変数を検索できます。
型認識ルールを使用するには、ESLint 構成をわずかに調整する必要があります。
module.exports = { extends: [ 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, root: true, };
typescript-eslint
の型推論を有効にするには、 parserOptions
を ESLint 構成に追加します。次に、 recommended
プリセットをrecommended-type-checked
に置き換えます。後者のプリセットには、約 17 個の新しい強力なルールが追加されます。この記事では、そのうちの 5 つにのみ焦点を当てます。
no-unsafe-argument
ルールは、 any
型の変数がパラメータとして渡される関数呼び出しを検索します。これが発生すると、型チェックが失われ、強い型付けの利点もすべて失われます。
たとえば、パラメータとしてオブジェクトを必要とするsaveForm
関数を考えてみましょう。 JSON を受信して解析し、 any
型を取得したとします。
// ❌ Incorrect function saveForm(values: FormValues) { console.log(values); } const formValues = JSON.parse(userInput); // ^? any saveForm(formValues); // ^ Unsafe argument of type `any` assigned // to a parameter of type `FormValues`.
このパラメーターを指定してsaveForm
関数を呼び出すと、 no-unsafe-argument
ルールによって安全でないというフラグが立てられ、 value
変数に適切な型を指定することが要求されます。
このルールは、関数の引数内のネストされたデータ構造を深く検査するのに十分強力です。したがって、関数の引数として渡すオブジェクトに型なしデータが含まれることは決してないと確信できます。
// ❌ Incorrect saveForm({ name: 'John', address: JSON.parse(addressJson), // ^ Unsafe assignment of an `any` value. });
このエラーを修正する最善の方法は、TypeScript の型の絞り込み、またはZodやSuperstructなどの検証ライブラリを使用することです。たとえば、解析されたデータの正確な種類を絞り込むparseFormValues
関数を作成してみましょう。
// ✅ Correct function parseFormValues(data: unknown): FormValues { if ( typeof data === 'object' && data !== null && 'name' in data && typeof data['name'] === 'string' && 'address' in data && typeof data.address === 'string' ) { const { name, address } = data; return { name, address }; } throw new Error('Failed to parse form values'); } const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues saveForm(formValues);
安全上の懸念がないため、 unknown
受け入れる関数に引数としてany
型を渡すことができることに注意してください。
データ検証関数の作成は、特に大量のデータを扱う場合には、面倒な作業になることがあります。したがって、データ検証ライブラリの使用を検討する価値があります。たとえば、Zod の場合、コードは次のようになります。
// ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); const formValues = schema.parse(JSON.parse(userInput)); // ^? { name: string, address: string } saveForm(formValues);
no-unsafe-assignment
ルールは、値がany
型である変数割り当てを検索します。このような代入により、実際にはデータの型が異なる可能性があるにもかかわらず、コンパイラは変数に特定の型があると誤解される可能性があります。
前述の JSON 解析の例を考えてみましょう。
// ❌ Incorrect const formValues = JSON.parse(userInput); // ^ Unsafe assignment of an `any` value
no-unsafe-assignment
ルールのおかげで、 formValues
他の場所に渡す前でも、 any
型をキャッチできます。修正戦略は変わりません。型の絞り込みを使用して、変数の値に特定の型を提供できます。
// ✅ Correct const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues
これら 2 つのルールがトリガーされる頻度はかなり低くなります。ただし、私の経験に基づくと、不適切に型指定されたサードパーティの依存関係を使用しようとする場合、これらは非常に役立ちます。
no-unsafe-member-access
ルールにより、変数がany
型の場合、 null
またはundefined
可能性があるため、オブジェクト プロパティにアクセスできなくなります。
no-unsafe-call
ルールにより、 any
型の変数は関数ではない可能性があるため、関数として呼び出すことができなくなります。
untyped-auth
という不適切に型指定されたサードパーティ ライブラリがあると想像してみましょう。
// ❌ Incorrect import { authenticate } from 'untyped-auth'; // ^? any const userInfo = authenticate(); // ^? any ^ Unsafe call of an `any` typed value. console.log(userInfo.name); // ^ Unsafe member access .name on an `any` value.
リンターは 2 つの問題を強調しています。
authenticate
関数の呼び出しは安全ではない可能性があります。userInfo
オブジェクトからname
プロパティを読み取ることは、認証が失敗するとnull
になるため、安全ではありません。
これらのエラーを修正する最善の方法は、厳密に型指定された API を備えたライブラリの使用を検討することです。ただし、これが不可能な場合は、ライブラリの種類を自分で拡張できます。固定ライブラリ タイプの例は次のようになります。
// ✅ Correct import { authenticate } from 'untyped-auth'; // ^? (login: string, password: string) => Promise<UserInfo | null> const userInfo = await authenticate('test', 'pwd'); // ^? UserInfo | null if (userInfo) { console.log(userInfo.name); }
no-unsafe-return
ルールは、より具体的なものを返す必要がある関数から誤ってany
型を返さないようにするのに役立ちます。このような場合、実際にはデータの型が異なる可能性があるにもかかわらず、コンパイラは戻り値が特定の型を持つと誤解する可能性があります。
たとえば、JSON を解析し、2 つのプロパティを持つオブジェクトを返す関数があるとします。
// ❌ Incorrect interface FormValues { name: string; address: string; } function parseForm(json: string): FormValues { return JSON.parse(json); // ^ Unsafe return of an `any` typed value. } const form = parseForm('null'); console.log(form.name); // ^ TypeError: Cannot read properties of null
parseForm
関数は、解析された値がチェックされないため、プログラムのどの部分でも使用されると実行時エラーが発生する可能性があります。 no-unsafe-return
ルールにより、このような実行時の問題が防止されます。
この問題は、解析された JSON が期待される型と一致することを確認する検証を追加することで簡単に修正できます。今回は Zod ライブラリを使用してみましょう。
// ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); function parseForm(json: string): FormValues { return schema.parse(JSON.parse(json)); }
型チェックされたルールを使用すると、すべての型を推論するために TypeScript のコンパイラを呼び出す必要があるため、ESLint のパフォーマンスが低下します。この速度の低下は主に、コミット前フックおよび CI でリンターを実行するときに顕著ですが、IDE で作業している場合には目立ちません。型チェックは IDE の起動時に 1 回実行され、コードを変更すると型が更新されます。
型を推論するだけの方が、通常のtsc
コンパイラの呼び出しよりも高速に動作することに注意してください。たとえば、約 150 万行の TypeScript コードを含む最新のプロジェクトでは、 tsc
による型チェックに約 11 分かかりますが、ESLint の型認識ルールのブートストラップに必要な追加時間はわずか約 2 分です。
私たちのチームにとって、型を認識した静的解析ルールを使用することによって得られる追加の安全性は、トレードオフの価値があります。小規模なプロジェクトでは、この決定はさらに簡単になります。
TypeScript プロジェクトでのany
の使用を制御することは、最適なタイプ セーフティとコード品質を達成するために重要です。 typescript-eslint
プラグインを利用することで、開発者はコードベース内のany
型の出現を特定して削除できるため、より堅牢で保守しやすいコードベースが得られます。
型を認識した eslint ルールを使用することにより、コードベース内にキーワードany
が出現しても、間違いや見落としではなく、意図的な決定となります。このアプローチにより、標準ライブラリやサードパーティの依存関係any
でなく、独自のコード内での使用も防止されます。
全体として、型認識リンターを使用すると、Java、Go、Rust などの静的型付けプログラミング言語と同様のレベルの型安全性を達成できます。これにより、大規模プロジェクトの開発とメンテナンスが大幅に簡素化されます。
この記事から何か新しいことを学んでいただければ幸いです。読んでくれてありがとう!