paint-brush
TypeScript を真に「厳密に型指定」するby@nodge
15,774
15,774

TypeScript を真に「厳密に型指定」する

Maksim Zemskov12m2023/09/10
Read on Terminal Reader

TypeScript は、データの形状が事前に不明な状況に備えて「Any」タイプを提供します。ただし、この型を過度に使用すると、型の安全性、コードの品質、開発者のエクスペリエンスに問題が生じる可能性があります。この記事では、「Any」型に関連するリスクを調査し、コードベースにその型が含まれる潜在的な原因を特定し、プロジェクト全体でその使用を制御するための戦略を提供します。
featured image - TypeScript を真に「厳密に型指定」する
Maksim Zemskov HackerNoon profile picture
0-item
1-item

TypeScript は、JavaScript 上に構築された厳密に型指定されたプログラミング言語であり、あらゆる規模で優れたツールを提供すると主張しています。ただし、 TypeScriptにはany型が含まれており、多くの場合、暗黙的にコードベースに侵入し、TypeScript の利点の多くが失われる可能性があります。


この記事では、TypeScript プロジェクトでany型を制御する方法について説明します。 TypeScript のパワーを解き放ち、究極のタイプ セーフを実現し、コードの品質を向上させる準備をしましょう。

TypeScript で Any を使用する場合の欠点

TypeScript は、開発者のエクスペリエンスと生産性を向上させるためのさまざまな追加ツールを提供します。


  • 開発段階の早い段階でエラーを発見するのに役立ちます。
  • コード エディターと IDE に優れた自動補完機能を提供します。
  • 素晴らしいコード ナビゲーション ツールと自動リファクタリングを通じて、大規模なコードベースのリファクタリングを簡単に行うことができます。
  • 追加のセマンティクスと型による明示的なデータ構造を提供することで、コードベースの理解を簡素化します。


ただし、コードベースで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 functionany型チェックを無効にするため、TypeScript はエラーを強調表示できません。
  • 2 番目のケースでは、 res2変数もany型を持ちます。これは、 anyを 1 回使用すると、コードベースの大部分に連鎖的な影響を与える可能性があることを意味します。


any使用しても問題がないのは、極端な場合またはプロトタイピングが必要な場合のみです。一般に、TypeScript を最大限に活用するには、 any使用も避けることをお勧めします。

Any 型の由来

any明示的に記述することが唯一の選択肢ではないため、コードベース内のany型のソースを認識することが重要です。 any型の使用を避けるための最善の努力にもかかわらず、any 型が暗黙的にコードベースに侵入する可能性があります。


コードベースには、 anyタイプの主なソースが 4 つあります。

  1. tsconfig のコンパイラ オプション。
  2. TypeScriptの標準ライブラリ。
  3. プロジェクトの依存関係。
  4. コードベース内でのanyの明示的な使用。


最初の 2 つの点については 、「tsconfig の主な考慮事項」と「標準ライブラリの型の改善」に関する記事をすでに書きました。プロジェクトのタイプ セーフティを改善したい場合は、ぜひチェックしてください。


今回は、コードベース内のany型の外観を制御するための自動ツールに焦点を当てます。

ステージ 1: ESLint の使用

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 回使用しただけでも、型推論によりコードベースの重要な部分に連鎖的な影響を与える可能性があるため、これは重要な目標です。しかし、これは究極の型安全性の達成にはまだ程遠いです。

no-explicit-any では不十分な理由

明示的に使用される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


変数pokemonssettings両方に暗黙的にanyタイプが与えられました。この場合、 no-explicit-anyも TypeScript の strict モードも警告しません。まだ。


これは、 response.json()JSON.parse()の型が TypeScript の標準ライブラリから取得されており、これらのメソッドには明示的なany注釈が付いているために発生します。変数に対してより適切な型を手動で指定することはできますが、標準ライブラリにはanyの型が 1,200 近く存在します。標準ライブラリからコードベースに侵入できるanyのケースを覚えておくことはほぼ不可能です。


外部依存関係についても同様です。 npm には型指定が不十分なライブラリが多数あり、そのほとんどは依然として JavaScript で書かれています。結果として、このようなライブラリを使用すると、コードベース内に暗黙的なanyが大量に発生する可能性があります。


一般に、コードに侵入any方法はまだたくさんあります。

ステージ 2: 型チェック機能の強化

理想的には、何らかの理由で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 の型の絞り込み、またはZodSuperstructなどの検証ライブラリを使用することです。たとえば、解析されたデータの正確な種類を絞り込む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 などの静的型付けプログラミング言語と同様のレベルの型安全性を達成できます。これにより、大規模プロジェクトの開発とメンテナンスが大幅に簡素化されます。


この記事から何か新しいことを学んでいただければ幸いです。読んでくれてありがとう!

役立つリンク