JavaScriptで静的型を使用する理由 (利点と欠点)

前半でたくさん話しました。 構文はこれで終わりです。最後に、楽しい部分に移りましょう。静的型を使用する利点と欠点を探ります。



利点その1:バグやエラーを事前に見つけることができます



静的型チェックにより、プログラムを起動することなく、定義した不変式がtrue



であることを確認できます。 また、これらの不変条件に違反がある場合、プログラムの開始前に検出され、操作中には検出されません。



小さな例:半径を取り、面積を計算する小さな関数があるとします:



 const calculateArea = (radius) => 3.14 * radius * radius; var area = calculateArea(3); // 28.26
      
      





さて、数値ではない関数(「攻撃者」など)に半径を渡したい場合...



 var area = calculateArea('im evil'); // NaN
      
      





NaN



を返します。 何らかの機能がcalculateArea



関数が常に数値を返すという事実に基づいている場合、これは脆弱性または失敗につながります。 あまり良くありませんよね?



静的型を使用する場合、渡されたパラメーターの特定の型を定義し、この関数の値を返します。



 const calculateArea = (radius: number): number => 3.14 * radius * radius;
      
      





次に、 calculateArea



関数の数以外の値を渡してみます-Flowは便利で素晴らしいメッセージを返します。



 calculateArea('Im evil'); ^^^^^^^^^^^^^^^^^^^^^^^^^ function call calculateArea('Im evil'); ^^^^^^^^^ string. This type is incompatible with const calculateArea = (radius: number): number => 3.14 * radius * radius; ^^^^^^ number
      
      





これで、関数は入力で有効な数字のみを受け入れ、有効な数字の形式でのみ結果を返すことが保証されます。



タイプコントローラーは、コードの作成時にエラーをすぐに通知するので、コードが顧客に送信された後にバグを見つけるよりもはるかに便利です(そしてはるかに安価です)。



アドバンテージNo. 2:ライブドキュメントがあります



タイプは、あなたと他の人の両方の生き生きとした呼吸のドキュメントとして機能します。



方法を理解するために、私がかつて作業した大きなコードベースで見つけたメソッドを見てみましょう。



 function calculatePayoutDate(quote, amount, paymentMethod) { let payoutDate; /* business logic */ return payoutDate; }
      
      





一見すると(2番目と3番目の両方)、この関数の使用方法は完全に不明です。



引用は数字ですか? またはブール値? 支払い方法はオブジェクトですか? または、支払い方法のタイプを表す文字列にすることもできますか? 関数は文字列形式で日付を返しますか? または、 Date



オブジェクトですか?



手がかりはありません。



そのとき、すべてのビジネスロジックを評価することにし、すべてを理解するまでコードベースでgrepを実行しました。 しかし、これは単純な関数がどのように機能するかを理解するためだけに多くの作業です。



一方、次のようなものを書いた場合:



 function calculatePayoutDate( quote: boolean, amount: number, paymentMethod: string): Date { let payoutDate; /* business logic */ return payoutDate; }
      
      





関数が受け入れるデータ型と返される型がすぐに明らかになります。 これは、静的型を使用して、関数何をしようとしているかを伝える方法の例です。 他の開発者に私たちが彼らに期待することを伝えることができ、彼らが私たちに期待するものを見ることができます。 次回、誰かがこの機能を使用する場合、質問はありません。



この問題はコードまたはドキュメントにコメントを追加することで解決されると主張できます。



 /* @function Determines the payout date for a purchase @param {boolean} quote - Is this for a price quote? @param {boolean} amount - Purchase amount @param {string} paymentMethod - Type of payment method used for this purchase */ function calculatePayoutDate(quote, amount, paymentMethod) { let payoutDate; /* .... Business logic .... */ return payoutDate; };
      
      





動作します。 しかし、もっとたくさんの言葉があります。 冗長性に加えて、コード内のそのようなコメントは信頼性が低く、構造がないため、維持するのが困難です-一部の開発者は良いコメントを書きますが、他の開発者はわかりにくいものを書き、コメントを残すのを忘れる場合があります。



リファクタリング後にコメントを更新するのを忘れるのは特に簡単です。 一方、型注釈は明確に定義された構文と構造を持ち、コード自体にエンコードされているため、陳腐化することはありません。



メリット#3:混乱を招くエラー処理が不要



型は、混乱を招くエラーのコード処理を排除するのに役立ちます。 calculateArea



関数に戻って、これがどのように発生するかを見てみましょう。



今回は、各半径の面積を計算するための半径の配列を彼女に渡します。



 const calculateAreas = (radii) => { var areas = []; for (let i = 0; i < radii.length; i++) { areas[i] = PI * (radii[i] * radii[i]); } return areas; };
      
      





この関数は機能しますが、無効な入力引数を正しく処理しません。 入力引数が有効な数値の配列でない場合に関数が状況を正しく処理することを確認したい場合は、次の形式の関数に到達します。



 const calculateAreas = (radii) => { // Handle undefined or null input if (!radii) { throw new Error("Argument is missing"); } // Handle non-array inputs if (!Array.isArray(radii)) { throw new Error("Argument must be an array"); } var areas = []; for (var i = 0; i < radii.length; i++) { if (typeof radii[i] !== "number") { throw new Error("Array must contain valid numbers only"); } else { areas[i] = 3.14 * (radii[i] * radii[i]); } } return areas; };
      
      





わあ このような小さな機能には多くのコードがあります。



そして静的型では、次のように書くだけです:



 const calculateAreas = (radii: Array<number>): Array<number> => { var areas = []; for (var i = 0; i < radii.length; i++) { areas[i] = 3.14 * (radii[i] * radii[i]); } return areas; };
      
      





これで、関数はエラー処理のために視覚的なゴミをすべて追加する前のように見えます。



静的型の利点を理解するのは簡単ですよね?



利点4:自信を持ってリファクタリングできる



これを人生の物語で説明します。 一度非常に大きなコードベースをUser



し、 User



クラスにインストールされたメソッドを更新する必要がありました。 特に、関数パラメーターの1つをstring



からobject



に変更しobject







変更を加えましたが、コミットを送信するのが怖かったです-この関数への呼び出しがコード全体に散らばっているので、すべてのインスタンスを正しく更新したかどうかわかりませんでした。 検証されていない補助ファイルのどこかにコールが残っている場合はどうなりますか?



確認する唯一の方法は、コードを送信し、大量のエラーで爆発しないように祈ることです。



静的型を使用する場合、この問題は発生しません。 そこで、心に平穏と静けさを感じるでしょう。関数と型の定義を更新すると、型コントローラがそこにあり、見逃す可能性のあるすべてのエラーを見つけることができます。 これらのタイプエラーを通過して修正するだけです。



利点5:データと動作の分離



静的型のめったに言及されていない利点の1つは、動作からデータを分離するのに役立つことです。



もう一度、静的型を使用したcalculateArea



関数を見てみましょう。



 const calculateAreas = (radii: Array<number>): Array<number> => { var areas = []; for (var i = 0; i < radii.length; i++) { areas[i] = 3.14 * (radii[i] * radii[i]); } return areas; };
      
      





この関数をどのようにコンパイルするかを考えてください。 データ型を指定するため、渡されるパラメーターの型を設定し、それに応じて値を返すことができるように、まず使用するデータ型について考える必要があります。







その後のみ、ロジックを実装します。







振る舞いとは別にデータを正確に表現する機能により、仮定を明確に示し、意図をより正確に伝えることができます。これにより、特定の精神的負担が取り除かれ、プログラマーに特定の明快さが与えられます。 それ以外の場合は、何らかの方法ですべてを念頭に置いておく必要があります。



アドバンテージ番号6:バグのカテゴリー全体の排除



プログラム実行中の型エラーは、JavaScript開発者が遭遇する最も一般的なエラーまたはバグの1つです。



たとえば、アプリケーションの初期状態が次のように設定されているとします。



 var appState = { isFetching: false, messages: [], };
      
      





次に、API呼び出しを行ってメッセージをappState



appState



するとします。 さらに、アプリケーションには表示用の非常に単純化されたコンポーネントがあり、 messages



(上記の状態で表示)を取得し、未読メッセージの数と各メッセージをリストの要素として表示します。



 import Message from './Message'; const MyComponent = ({ messages }) => { return ( <div> <h1> You have { messages.length } unread messages </h1> { messages.map(message => <Message message={ message } /> )} </div> ); };
      
      





メッセージを収集するためのAPI呼び出しが機能しないかundefined



返した場合、本番環境でタイプエラーが発生します。



 TypeError: Cannot read property 'length' of undefined
      
      





...プログラムは失敗します。 あなたは顧客を失います。 カーテン。



静的型がどのように役立つかを見てみましょう。 アプリケーションの状態にフロータイプを追加することから始めましょう。 AppState



型のエイリアスを使用して状態を判断します。



 type AppState = { isFetching: boolean, messages: ?Array<string> }; var appState: AppState = { isFetching: false, messages: null, };
      
      





メッセージを取得するためのAPIが確実に機能しないことが知られているため、 messages



の値の文字列配列に示す場合がありmessages







前回と同様に、信頼できないAPIを介してメッセージを取得し、ビューコンポーネントで使用します。



 import Message from './Message'; const MyComponent = ({ messages }) => { return ( <div> <h1> You have { messages.length } unread messages </h1> { messages.map(message => <Message message={ message } /> )} </div> ); };
      
      





しかし、この時点で、Flowはエラーを検出して不平を言います。



 <h1> You have {messages.length} unread messages </h1> ^^^^^^ property `length`. Property cannot be accessed on possibly null value <h1> You have {messages.length} unread messages </h1> ^^^^^^^^ null <h1> You have {messages.length} unread messages </h1> ^^^^^^ property `length`. Property cannot be accessed on possibly undefined value <h1> You have {messages.length} unread messages </h1> ^^^^^^^^ undefined { messages.map(message => <Message message={ message } /> )} ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call of method `map`. Method cannot be called on possibly null value { messages.map(message => <Message message={ message } /> )} ^^^^^^^^ null { messages.map(message => <Message message={ message } /> )} ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call of method `map`. Method cannot be called on possibly undefined value { messages.map(message => <Message message={ message } /> )} ^^^^^^^^ undefined
      
      





待って!



messages



maybe



として定義したので、 messages



null



またはundefined



ます。 ただし、これは、 messages



の値が実際にnull



またはundefined



である場合、操作を実行しようとすると型エラーがポップアップするため、 null



をチェックせずに操作( .length



.map



)を実行する権利を与えません。



戻って関数を更新し、次のようなものを表示しましょう。



 const MyComponent = ({ messages, isFetching }: AppState) => { if (isFetching) { return <div> Loading... </div> } else if (messages === null || messages === undefined) { return <div> Failed to load messages. Try again. </div> } else { return ( <div> <h1> You have { messages.length } unread messages </h1> { messages.map(message => <Message message={ message } /> )} </div> ); } };
      
      





これで、Flowは、 messages



null



またはundefined



であるすべての状況を考慮したことを認識しているため、コードタイプチェックはエラー0で失敗します。 さようなら、プログラム実行中のエラー!



メリット#7:単体テストの削減



静的型は、関数と戻り値に渡されるパラメーターの型を保証するため、解析されたエラーを取り除くのにどのように役立つかを見てきました。 その結果、静的型は単体テストの数も減らします。



たとえば、動的型とエラー処理を備えたcalculateAreas



関数に戻ります。



 const calculateAreas = (radii) => { // Handle undefined or null input if (!radii) { throw new Error("Argument is missing"); } // Handle non-array inputs if (!Array.isArray(radii)) { throw new Error("Argument must be an array"); } var areas = []; for (var i = 0; i < radii.length; i++) { if (typeof radii[i] !== "number") { throw new Error("Array must contain valid numbers only"); } else { areas[i] = 3.14 * (radii[i] * radii[i]); } } return areas; };
      
      





勤勉なプログラマーであれば、無効なパラメーターをテストして、プログラムによって正しく処理されていることを確認することを検討できます。



 it('should not work - case 1', () => { expect(() => calculateAreas([null, 1.2])).to.throw(Error); }); it('should not work - case 2', () => { expect(() => calculateAreas(undefined).to.throw(Error); }); it('should not work - case 2', () => { expect(() => calculateAreas('hello')).to.throw(Error); });
      
      





...など。 しかし、いくつかの境界ケースをテストすることを忘れる可能性が非常に高く、問題を見つけるのはお客様です。 :(



テストは、テストするために思いついた状況にのみ基づいているため、実存的であり、実際には簡単に回避できます。



一方、タイプを設定する必要がある場合:



 const calculateAreas = (radii: Array<number>): Array<number> => { var areas = []; for (var i = 0; i < radii.length; i++) { areas[i] = 3.14 * (radii[i] * radii[i]); } return areas; };
      
      





...私たちは目標が真実であるという保証を得るだけでなく、そのようなテストは単により信頼できるものです。 経験的テストとは異なり、型は普遍的であり、回避が困難です。



一般的に、図は次のとおりです。テストはロジックのチェックに適し、タイプはデータ型のチェックに適しています。 それらを組み合わせると、部品の合計がさらに大きな効果をもたらします。



メリット#8:ドメインモデリングツール



静的型を使用する私のお気に入りの例の1つは、ドメインモデリングです。 この場合、データとこのデータに対するプログラムの動作の両方を含むモデルが作成されます。 この場合、型の使用方法の例を理解することが最善です。



アプリケーションで、プラットフォームで購入するための1つ以上の支払い方法がユーザーに提供されているとします。 ユーザーは、3つの支払い方法(Paypal、クレジットカード、銀行口座)から選択できます。



そのため、最初に3つの支払い方法に型エイリアスを適用します。



 type Paypal = { id: number, type: 'Paypal' }; type CreditCard = { id: number, type: 'CreditCard' }; type Bank = { id: number, type: 'Bank' };
      
      





これで、 PaymentMethod



タイプを、3つのケースを持つPaymentMethod



集合として設定できます。



 type PaymentMethod = Paypal | CreditCard | Bank;
      
      





次に、アプリケーションの状態のモデルを作成しましょう。 複雑にならないように、これらのアプリケーションは、ユーザーが利用できる支払い方法のみで構成されていると仮定します。



 type Model = { paymentMethods: Array<PaymentMethod> };
      
      





それは受け入れられますか? ユーザーの支払い方法を受け取るには、APIにリクエストを送信する必要があります。プロセスの結果と段階に応じて、アプリケーションは異なる状態をとることがあります。 実際には、次の4つの状態が可能です。



1)支払い方法を受け取りませんでした。

2)支払い方法を受け取る処理中です。

3)お支払い方法を受け取りました。

4)支払い方法を取得しようとしましたが、エラーが発生しました。



しかし、 paymentMethods



した単純なModel



タイプは、これらのすべてのケースをカバーしているpaymentMethods



はありません。 代わりに、 paymentMethods



常に存在すると想定しています。



うーん アプリケーションの状態がこれらの4つの値のいずれか1つだけを取るようにモデルを構築する方法はありますか? 見てみましょう:



 type AppState<E, D> = { type: 'NotFetched' } | { type: 'Fetching' } | { type: 'Failure', error: E } | { type: 'Success', paymentMethods: Array<D> };
      
      





AppState



を上記の4つの状態のいずれかに設定するために、互いに素なセットタイプを使用しました。 type



プロパティを使用して、アプリケーションが4つの状態のどれにあるかを判断する方法に注目してください。 ばらばらのセットを作成するのはこのtype



プロパティです。 これを使用して、支払い方法がある場合とない場合を分析して判断できます。



また、パラメーター化されたタイプE



およびD



をアプリケーション状態に渡すことにも気付くでしょう。 タイプD



は、ユーザーの支払い方法(上記で定義したPaymentMethod



)になります。 エラーのタイプとなるタイプE



設定していないため、ここでそれを実行しましょう。



 type HttpError = { id: string, message: string };
      
      





これで、アプリケーションドメインをシミュレートできます。



 type Model = AppState<HttpError, PaymentMethod>;
      
      





一般に、アプリケーション状態の署名はAppState<E, D>



になりました。ここで、 E



HttpError



の形式で、 D



PaymentMethod



です。 また、 AppState



は、 NotFetched



Fetching



Failure



Success



4つの状態(これら4つのみ)があります。







このようなドメインモデルは、特定のビジネスルールに従ってユーザーインターフェイスを考え、開発するのに役立ちます。 ビジネスルールは、アプリケーションはこれらの状態のいずれか1つにしかなれないことを示しており、これによりAppStateを明示的に表し、これらの定義済みの状態の1つにのみなることを保証できます。 そして、このモデルに従って開発する場合(たとえば、表示用のコンポーネントを作成する場合)、4つの可能な状態すべてを処理する必要があることが明らかになります。



さらに、コードはそれ自体を文書化します-互いに素なセットを見るだけで、AppStateがどのように構成されているかがすぐにわかります。



静的型を使用する場合の欠点



生活やプログラミングの他のすべてと同様に、静的型のチェックにはいくつかの妥協が必要です。



これらの欠点を理解し、認識することは重要です。静的型を使用することが理にかなっている場合、および単に価値がない場合に、十分な情報に基づいた決定を下せるようにするためです。



これらの考慮事項の一部を次に示します。



欠陥#1:静的型には事前の調査が必要



JavaScriptが初心者にとってとても素晴らしい言語である理由の1つは、初心者が生産的な作業を始める前に完全な型システムを学ぶ必要がないことです。



私が初めてElm(静的型付けを備えた関数型言語)を学んだとき、型はしばしば邪魔になりました。 型定義が原因でコンパイラエラーが常に発生しました。



型の効果的な使用を学ぶことは、言語自体を学ぶことに成功した半分でした。 その結果、静的型のため、Elmの学習曲線はJavaScriptよりも急です。



これは、構文を学習することで認知的負荷が最も大きい初心者にとって特に重要です。 このセットに構文を追加すると、初心者を圧倒する可能性があります。



欠陥#2:冗長性に巻き込まれる



静的型のため、プログラムはより冗長で混乱しているように見えることがよくあります。



たとえば、代わりに:



 async function amountExceedsPurchaseLimit(amount, getPurchaseLimit){ var limit = await getPurchaseLimit(); return limit > amount; }
      
      





書かなければなりません:



 async function amountExceedsPurchaseLimit( amount: number, getPurchaseLimit: () => Promise<number> ): Promise<boolean> { var limit = await getPurchaseLimit(); return limit > amount; }
      
      





そして代わりに:



 var user = { id: 123456, name: 'Preethi', city: 'San Francisco', };
      
      





私はこれを書かなければなりません:



 type User = { id: number, name: string, city: string, }; var user: User = { id: 123456, name: 'Preethi', city: 'San Francisco', };
      
      





明らかに、余分なコード行が追加されます。 しかし、これを欠陥と見なすことに反対する議論がいくつかあります。



まず、前述したように、静的型はテストのカテゴリ全体を破壊します。 一部の開発者は、これが完全に合理的な妥協案であると感じるかもしれません。



第二に、先ほど見たように、静的型は複雑なエラーを処理する必要性を排除できる場合があり、これによりコードの混乱が大幅に削減されます。



冗長性が型に対する真の議論であるかどうかを言うのは難しいですが、覚えておく価値はあります。



欠陥3:型の使用を習得するには時間がかかる



プログラムで最適なタイプを選択する方法を学ぶには、多くの時間と練習が必要です。さらに、静的に監視する必要があるもの、および動的な形で残す方が良いものを適切に開発するには、正確なアプローチ、実践、および経験も必要です。



たとえば、1つのアプローチは、重要なビジネスロジックを静的型でエンコードしますが、不必要な複雑さを回避するために、短期または重要でないロジックを動的なままにします。



特に経験の浅い開発者がその場で決定を下さなければならない場合、違いを理解することは困難です。



欠陥4:静的な型は高速開発を遅らせる可能性がある



前に述べたように、私はElmを勉強したとき、特にコードを追加したり変更したりしたときに、型について少しつまずきました。コンパイラーのエラーに常に気を取られて、作業を行って進捗を感じることは困難です。



ここでの議論は、静的型をチェックするために、プログラマーが集中力を失いすぎることがあるということです-そして、あなたが知っているように、集中力は良いプログラムを書くための重要な要素です。



これが唯一のポイントではありません。静的タイプのコントローラーも常に理想的ではありません。何をすべきかを知っているときに状況が発生し、タイプチェックが干渉することがあります。



私は他のいくつかの欠点を逃したと確信していますが、これらは私にとって最も重要です。



JavaScriptで静的型を使用する必要がありますか?







私が最初に学んだプログラミング言語はJavaScriptとPythonであり、どちらも動的型付けを備えた言語です。



しかし、静的型をマスターすると、プログラミングについての考え方に新しい次元が追加されました。たとえば、最初はElmのコンパイラエラーの絶え間ない報告が圧倒的でしたが、型検出と「コンパイラに満足」が2番目の性質になり、実際にプログラミングスキルが向上しました。さらに、私が何か間違ったことをしていて、その修正方法を教えてくれるスマートロボットほど解放的なものはありません。



はい、過度の冗長性やそれらの研究に時間を費やす必要性など、静的型の避けられない妥協があります。しかし、型はプログラムにセキュリティと正確さを追加し、個人的にこれらの「欠点」の重要性を排除します。



動的タイプはより高速で単純に見えますが、実際にプログラムを実行すると失敗する可能性があります。同時に、より複雑なパラメーター化された型を扱うJava開発者と話すことができます。



最終的に、普遍的な解決策はありません。個人的には、次の条件下で静的型を使用することを好みます。



  1. このプログラムはあなたのビジネスにとって重要です。
  2. このプログラムは、新しいニーズに合わせてリファクタリングされる可能性があります。
  3. プログラムは複雑で、多くの可動部分があります。
  4. このプログラムは、コードを迅速かつ正確に理解する必要がある多数の開発者グループによってサポートされています。


一方、次の条件では静的型を拒否します。



  1. コードは短命であり、重要ではありません。
  2. プロトタイプを作成し、できるだけ早く進めようとします。
  3. プログラムは小さく、シンプルです。
  4. あなただけが開発者です。


最近のJavaScript開発の利点は、FlowやTypeScriptなどのツールのおかげで、最終的に静的型または古き良きJavaScriptを使用する選択ができることです。



おわりに



これらの記事が、型の重要性、それらの使用方法、そして最も重要なのは*いつ*それらを使用するかを理解するのに役立つことを願っています。



動的型と静的型を切り替える機能は、JavaScriptコミュニティにとって強力なツールであり、エキサイティングです:)



著者について:カリフォルニア州サピエンAIの共同設立者兼リードエンジニアであるPreethi Kasireddy







All Articles