TypeScriptとJavaScript:あなたの頼りになるガイド

公開: 2022-03-11

TypeScriptまたはJavaScript? 開発者は、グリーンフィールドWebまたはNode.jsプロジェクトでこの選択を検討していますが、既存のプロジェクトでも検討する価値のある質問です。 JavaScriptのスーパーセットであるTypeScriptは、JavaScriptのすべての機能に加えて、いくつかの追加の特典を提供します。 TypeScriptは本質的に、コードをクリーンにコーディングすることを奨励し、コードをよりスケーラブルにします。 ただし、プロジェクトには必要なだけプレーンなJavaScriptを含めることができるため、TypeScriptを使用することはオールオアナッシングの提案ではありません。

TypeScriptとJavaScriptの関係

TypeScriptはJavaScriptに明示的な型システムを追加し、変数型の厳密な適用を可能にします。 TypeScriptは、トランスパイル中に型チェックを実行します。これは、TypeScriptコードをJavaScriptコードに変換するコンパイルの形式であり、WebブラウザーとNode.jsが理解します。

TypeScriptとJavaScriptの例

有効なJavaScriptスニペットから始めましょう。

 let var1 = "Hello"; var1 = 10; console.log(var1);

ここで、 var1stringとして始まり、次にnumberになります。

JavaScriptは大まかに型付けされているだけなので、 var1は、文字列から関数まで、いつでも任意の型の変数として再定義できます。

このコードを実行すると、 10が出力されます。

それでは、このコードをTypeScriptに変更しましょう。

 let var1: string = "Hello"; var1 = 10; console.log(var1);

この場合、 var1stringとして宣言します。 次に、TypeScriptの厳密な型システムでは許可されていない番号を割り当てようとします。 トランスパイルするとエラーが発生します。

 TSError: ⨯ Unable to compile TypeScript: src/snippet1.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'. 2 var1 = 10;

元のJavaScriptスニペットをTypeScriptであるかのように扱うようにトランスパイラーに指示した場合、トランスパイラーはvar1string | numberである必要があると自動的に推測します。 string | number 。 これはTypeScriptの共用体型であり、 var1にいつでもstringまたはnumberを割り当てることができます。 タイプの競合を解決すると、TypeScriptコードは正常にトランスパイルされます。 これを実行すると、JavaScriptの例と同じ結果が得られます。

TypeScriptとJavaScriptの比較30,000フィートから:スケーラビリティの課題

JavaScriptは至る所に存在し、あらゆる規模のプロジェクトに力を与えており、1990年代の初期には想像もできなかった方法で適用されています。 JavaScriptは成熟していますが、スケーラビリティのサポートに関しては不十分です。 したがって、開発者は、規模と複雑さの両方で成長したJavaScriptアプリケーションに取り組んでいます。

ありがたいことに、TypeScriptはJavaScriptプロジェクトのスケーリングに関する多くの問題に対処します。 検証、リファクタリング、文書化という上位3つの課題に焦点を当てます。

検証

新しいコードの追加、変更、テストなどのタスクを支援するために統合開発環境(IDE)に依存していますが、IDEは純粋なJavaScript参照を検証できません。 変数と関数名のタイプミスの可能性を回避するためにコーディングする際に注意深く監視することで、この欠点を軽減します。

コードがサードパーティから発信された場合、問題の大きさは指数関数的に増大します。サードパーティでは、めったに実行されないコードブランチで壊れた参照が簡単に検出されなくなる可能性があります。

対照的に、TypeScriptを使用すると、コーディングに集中でき、トランスパイル時にエラーが識別されることを確信できます。 これを実証するために、いくつかのレガシーJavaScriptコードから始めましょう。

 const moment = require('moment'); const printCurrentTime = (format) => { if (format === 'ISO'){ console.log("Current ISO TS:", moment().toISO()); } else { console.log("Current TS: ", moment().format(format)); } }

.toISO()呼び出しは、moment.js toISOString()メソッドのタイプミスですが、 format引数がISOでない場合、コードは機能します。 初めてISOを関数に渡そうとすると、次のランタイムエラーが発生しTypeError: moment(...).toISO is not a function

スペルミスのあるコードを見つけるのは難しいかもしれません。 現在のコードベースには破線へのパスがない可能性があります。その場合、壊れた.toISO()参照はテストで検出されません。

このコードをTypeScriptに移植すると、IDEは壊れた参照を強調表示し、修正を求めるプロンプトを表示します。 何もせずにトランスパイルしようとすると、ブロックされ、トランスパイラーは次のエラーを生成します。

 TSError: ⨯ Unable to compile TypeScript: src/catching-mistakes-at-compile-time.ts:5:49 - error TS2339: Property 'toISO' does not exist on type 'Moment'. 5 console.log("Current ISO TS:", moment().toISO());

リファクタリング

サードパーティのコード参照のタイプミスは珍しいことではありませんが、次のように、内部参照のタイプミスに関連するさまざまな問題があります。

 const myPhoneFunction = (opts) => { // ... if (opts.phoneNumbr) doStuff(); }

唯一の開発者は、 phoneNumbrのすべてのインスタンスを見つけて修正し、 erに終了することができます。

しかし、チームが大きくなればなるほど、この単純でよくある間違いは不当にコストがかかります。 作業を実行する過程で、同僚はそのようなタイプミスを認識して広める必要があります。 または、両方のスペルをサポートするコードを追加すると、コードベースが不必要に肥大化してしまいます。

TypeScriptを使用すると、タイプミスを修正すると、依存コードがトランスパイルしなくなり、修正をコードに伝播するように同僚に通知されます。

ドキュメンテーション

正確で関連性のあるドキュメントは、開発者のチーム内およびチーム間のコミュニケーションの鍵となります。 JavaScript開発者は、JSDocを使用して、予想されるメソッドとプロパティのタイプを文書化することがよくあります。

TypeScriptの言語機能(抽象クラ​​ス、インターフェイス、型定義など)は、契約ごとのプログラミングを容易にし、高品質のドキュメントを作成します。 さらに、オブジェクトが準拠しなければならないメソッドとプロパティの正式な定義を持つことは、重大な変更を識別し、テストを作成し、コードのイントロスペクションを実行し、アーキテクチャパターンを実装するのに役立ちます。

TypeScriptの場合、頼りになるツールTypeDoc(TSDoc提案に基づく)は、コードから型情報(クラス、インターフェイス、メソッド、プロパティなど)を自動的に抽出します。 したがって、JSDocよりもはるかに包括的なドキュメントを簡単に作成できます。

TypeScriptとJavaScriptの利点

それでは、TypeScriptを使用してこれらのスケーラビリティの課題に対処する方法を見ていきましょう。

高度なコード/リファクタリングの提案

多くのIDEは、TypeScript型システムからの情報を処理でき、コーディング時に参照検証を提供します。 さらに良いことに、入力すると、IDEは、参照に関連する一目でわかるドキュメント(たとえば、関数が期待する引数)を提供し、コンテキストに応じて正しい変数名を提案できます。

このTypeScriptスニペットでは、IDEは、関数の戻り値内のキーの名前のオートコンプリートを提案しています。

 /** * Simple function to parse a CSV containing people info. * @param data A string containing a CSV with 3 fields: name, surname, age. */ const parsePeopleData = (data: string) => { const people: {name: string, surname: string, age: number}[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; const tokens = row.split(',').map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3){ errors.push(`Row "${row}" contains only ${tokens.length} tokens. 3 required`); continue; } people.push({ name: tokens[0], surname: tokens[1], age: +tokens[2] }) } return {people, errors}; }; const exampleData = ` Gordon,Freeman,27 G,Man,99 Alyx,Vance,24 Invalid Row,, Again, Invalid `; const result = parsePeopleData(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }

私のIDEであるVisualStudioCodeは、関数の呼び出しを開始したときに(コールアウトで)この提案を提供しました(31行目)。

parsePeopleData()と入力すると、IDEはTypeScriptトランスパイラーから「parsePeopleData(data:string):{people:{name:string; surname:string; age:number;} [];エラー:」というツールチップを表示します。 string [];} "の後に、関数定義の前の複数行コメントに含まれるテキスト、" name、surname、ageの3つのフィールドを持つCSVを含む文字列。人の情報を含むCSVを解析する単純な関数。 "

さらに、IDEのオートコンプリートの提案(コールアウト内)はコンテキスト的に正しく、ネストされたキーの状況内で有効な名前のみを表示します(34行目)。

「map(p => `Name:$ {p。」」と入力するとポップアップ表示される3つの提案(年齢、名前、名前)。最初の提案が強調表示され、横に「(プロパティ)年齢:番号」が表示されます。

このようなリアルタイムの提案は、より迅速なコーディングにつながります。 さらに、IDEは、TypeScriptの厳密な型情報に依存して、任意のスケールでコードをリファクタリングできます。 プロパティの名前の変更、ファイルの場所の変更、スーパークラスの抽出などの操作は、参照の正確性に100%自信がある場合は簡単になります。

インターフェイスのサポート

JavaScriptとは対照的に、TypeScriptは、インターフェースを使用して型を定義する機能を提供します。 インターフェイスは、オブジェクトに含める必要のあるメソッドとプロパティを正式にリストしますが、実装はしません。 この言語構​​成は、他の開発者とのコラボレーションに特に役立ちます。

次の例は、TypeScriptの機能を活用して、一般的なOOPパターン(この場合は戦略責任の連鎖)を適切に実装する方法を示しています。これにより、前の例が改善されます。

 export class PersonInfo { constructor( public name: string, public surname: string, public age: number ){} } export interface ParserStrategy{ /** * Parse a line if able. * @returns The parsed line or null if the format is not recognized. */ (line: string): PersonInfo | null; } export class PersonInfoParser{ public strategies: ParserStrategy[] = []; parse(data: string){ const people: PersonInfo[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; let parsed; for (let s of this.strategies){ parsed = s(row); if (parsed) break; } if (!parsed){ errors.push(`Unable to find a strategy capable of parsing "${row}"`); } else { people.push(parsed); } } return {people, errors}; } } const exampleData = ` Gordon,Freeman,27 G;Man;99 {"name":"Alyx", "surname":"Vance", "age":24} Invalid Row,, Again, Invalid `; const parser = new PersonInfoParser(); const createCSVStrategy = (fieldSeparator = ','): ParserStrategy => (line) => { const tokens = line.split(fieldSeparator).map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3) return null; return new PersonInfo(tokens[0], tokens[1], +tokens[2]); }; parser.strategies.push( (line) => { try { const {name, surname, age} = JSON.parse(line); return new PersonInfo(name, surname, age); } catch(err){ return null; } }, createCSVStrategy(), createCSVStrategy(';') ); const result = parser.parse(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }

ES6モジュール-どこでも

この記事の執筆時点では、すべてのフロントエンドおよびバックエンドのJavaScriptランタイムがES6モジュールをサポートしているわけではありません。 ただし、TypeScriptでは、ES6モジュール構文を使用できます。

 import * as _ from 'lodash'; export const exampleFn = () => console.log(_.reverse(['a', 'b', 'c']));

トランスパイルされた出力は、選択した環境と互換性があります。 たとえば、コンパイラオプション--module CommonJSを使用すると、次のようになります。

 "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn;

代わりに--module UMDを使用すると、TypeScriptはより詳細なUMDパターンを出力します。

 (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "lodash"], factory); } })(function (require, exports) { "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; });

ES6クラス-どこでも

レガシー環境では、ES6クラスのサポートが不足していることがよくあります。 TypeScriptトランスパイルは、ターゲット固有の構造を使用して互換性を確保します。 TypeScriptソーススニペットは次のとおりです。

 export class TestClass { hello = 'World'; }

JavaScriptの出力は、TypeScriptで指定できるモジュールとターゲットの両方に依存します。

--module CommonJS --target es3が生成するものは次のとおりです。

 "use strict"; exports.__esModule = true; exports.TestClass = void 0; var TestClass = /** @class */ (function () { function TestClass() { this.hello = 'World'; } return TestClass; }()); exports.TestClass = TestClass;

代わりに--module CommonJS --target es6を使用すると、次のトランスパイル結果が得られます。 classキーワードは、ES6をターゲットにするために使用されます。

 "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestClass = void 0; class TestClass { constructor() { this.hello = 'World'; } } exports.TestClass = TestClass;

非同期/待機機能-どこでも

Async / awaitを使用すると、非同期JavaScriptコードの理解と保守が容易になります。 TypeScriptは、非同期/待機をネイティブに提供しないランタイムを含め、すべてのランタイムにこの機能を提供します。

ES3やES5などの古いランタイムでasync/awaitを実行するには、 Promiseベースの出力(BluebirdやES2015ポリフィルなど)の外部サポートが必要になることに注意してください。 TypeScriptに同梱されているPromiseポリフィルは、トランスパイルされた出力に簡単に統合できます。それに応じて、 libコンパイラオプションを構成する必要があります。

プライベートクラスフィールドのサポート-どこでも

従来のターゲットの場合でも、TypeScriptは、強く型付けされた言語(JavaやC#など)とほぼ同じ方法でprivateフィールドをサポートします。 対照的に、多くのJavaScriptランタイムは、ES2022の完成した提案であるハッシュプレフィックス構文を介してprivateフィールドをサポートします。

TypeScriptとJavaScriptのデメリット

TypeScriptを実装することの主な利点を強調したので、TypeScriptが適切でない可能性があるシナリオを調べてみましょう。

トランスパイル:ワークフローの非互換性の可能性

特定のワークフローまたはプロジェクト要件は、TypeScriptのトランスパイルステップと互換性がない場合があります。たとえば、展開後に外部ツールを使用してコードを変更する必要がある場合、または生成される出力が開発者にとって使いやすいものである必要がある場合です。

たとえば、最近、Node.js環境用のAWSLambda関数を作成しました。 トランスパイルを要求すると、私や他のチームメンバーがAWSオンラインエディターを使用して関数を編集できなくなるため、TypeScriptは適切ではありませんでした。 これは、プロジェクトマネージャーにとって大きな問題でした。

型システムはトランスパイル時間までしか機能しません

TypeScriptのJavaScript出力には型情報が含まれていないため、型チェックは実行されないため、実行時に型の安全性が損なわれる可能性があります。 たとえば、関数が常にオブジェクトを返すように定義されているとします。 .jsファイル内での使用からnullが返されると、ランタイムエラーが発生します。

タイプ情報に依存する機能(プライベートフィールド、インターフェイス、ジェネリックなど)は、プロジェクトに付加価値をもたらしますが、トランスパイル中に削除されます。 たとえば、 privateクラスのメンバーは、トランスパイル後にプライベートではなくなります。 明確にするために、この性質の実行時の問題はTypeScriptに固有のものではなく、JavaScriptでも同じ問題が発生することが予想されます。

TypeScriptとJavaScriptの組み合わせ

TypeScriptには多くの利点がありますが、JavaScriptプロジェクト全体を一度に変換することを正当化できない場合があります。 幸い、TypeScriptトランスパイラーにファイルごとにプレーンJavaScriptとして解釈するものを指定できます。 実際、このハイブリッドアプローチは、プロジェクトのライフサイクルの過程で発生する個々の課題を軽減するのに役立ちます。

コードが次の場合、JavaScriptを変更しないでおくことをお勧めします。

  • 元同僚によって書かれたものであり、TypeScriptに変換するにはかなりのリバースエンジニアリング作業が必要になります。
  • TypeScriptで許可されていない手法を使用し(たとえば、オブジェクトのインスタンス化後にプロパティを追加する)、TypeScriptルールに準拠するためにリファクタリングが必要になります。
  • JavaScriptを使い続ける別のチームに所属しています。

このような場合、宣言ファイル( .d.tsファイル。定義ファイルまたは型付けファイルと呼ばれることもあります)は、JavaScriptコードをそのままにして、IDEの提案を可能にするのに十分な型データをTypeScriptに提供します。

多くのJavaScriptライブラリ(Lodash、Jest、Reactなど)は、TypeScriptタイピングファイルを個別のタイプパッケージで提供しますが、他のライブラリ(Moment.js、Axios、Luxonなど)は、タイピングファイルをメインパッケージに統合します。

TypeScriptとJavaScript:合理化とスケーラビリティの問題

TypeScriptを通じて利用できる比類のないサポート、柔軟性、および拡張機能により、開発者のエクスペリエンスが大幅に向上し、プロジェクトやチームの拡張が可能になります。 TypeScriptをプロジェクトに組み込むための主なコストは、トランスピレーションビルドステップの追加です。 ほとんどのアプリケーションでは、JavaScriptへのトランスパイルは問題ではありません。 むしろ、それはTypeScriptの多くの利点への足がかりです。


Toptal Engineeringブログでさらに読む:

  • TypeScriptとJestサポートの操作:AWSSAMチュートリアル