TypeScript School of Magic:ジェネリックとタイプ拡張

今日翻訳している記事の著者は、TypeScriptは素晴らしいと言っています。 TSを初めて使い始めたとき、彼はこの言語に内在する自由が本当に好きでした。 プログラマーがTS固有のメカニズムを使用して作業に費やす労力が大きいほど、得られるメリットは大きくなります。 その後、彼はタイプ注釈を定期的にのみ使用しました。 時々、彼はコード補完とコンパイラーのヒントの機会を利用しましたが、主に彼が解決したタスクの彼自身のビジョンのみに依存していました。

時間が経つにつれて、この資料の著者は、コンパイル段階で検出されたエラーをバイパスするたびに、プログラムの実行中に爆発する可能性のある時限爆弾をコードに入れることに気付きました。 単純な構造を使用してエラーに「苦労」するたびに、何時間ものハードデバッグでその費用を支払う必要がありました。



最後に、彼はそうしないほうが良いという結論に達しました。 彼はコンパイラと友達になり、彼のヒントに注意を払い始めました。 コンパイラーは、コード内の問題を検出し、実際の害を引き起こす可能性があるずっと前に報告します。 この記事の著者は、自分自身を開発者と見なしており、コンパイラーが自分から自分を保護しているため、コンパイラーが彼の親友であることに気付きました。 アルバス・ダンブルドアの言葉を思い出せないのは、「敵に立ち向かうには多くの勇気が必要ですが、友達に立ち向かうにはそれ以上の勇気が必要です」。

コンパイラがどれほど優れていても、喜ばせるのは必ずしも簡単ではありません。 タイプanyの使用を避けることany非常に難しいanyあります。 そして、時にはそれが何らかの問題に対する唯一の合理的な解決策であると思われany

この資料では、2つの状況に焦点を当てています。 それらの中で型の使用を避けることにより、コードの型安全性を確保し、その再利用の可能性を開き、直感的にすることができます。

ジェネリック


学校のデータベースに取り組んでいるとします。 非常に便利なヘルパー関数getBy 。 生徒を名前で表すオブジェクトを取得するには、 getBy(model, "name", "Harry")という形式のコマンドを使用できます。 このメカニズムの実装を見てみましょう(ここでは、コードを複雑にしないために、データベースは通常の配列で表されています)。

 type Student = { name: string; age: number; hasScar: boolean; }; const students: Student[] = [ { name: "Harry", age: 17, hasScar: true }, { name: "Ron", age: 17, hasScar: false }, { name: "Hermione", age: 16, hasScar: false } ]; function getBy(model, prop, value) {   return model.filter(item => item[prop] === value)[0] } 

ご覧のとおり、優れた関数がありますが、型注釈は使用されません。また、それらが存在しないことは、そのような関数を型セーフと呼ぶことができないことも意味します。 修正してください。

 function getBy(model: Student[], prop: string, value): Student | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "name", "Hermione") // result: Student 

したがって、私たちの機能はすでにずっと良く見えます。 コンパイラーは、期待される結果のタイプを認識します。これは後で便利になります。 ただし、型の安全な作業を実現するために、関数を再利用する可能性を犠牲にしました。 他のエンティティを取得するために使用する必要がある場合はどうなりますか? この機能を改善できないということはあり得ません。 そして、本当にそうです。

TypeScriptでは、他の強く型付けされた言語と同様に、ジェネリックを使用できます。ジェネリックは、「ジェネリック型」、「ユニバーサル型」、「ジェネラリゼーション」とも呼ばれます。

ジェネリックは通常の変数に似ていますが、値の代わりに型定義が含まれています。 Student型の代わりに汎用型T使用するように、関数のコードを書き直しますT

 function getBy<T>(model: T[], prop: string, value): T | null {   return model.filter(item => item[prop] === value)[0] } const result = getBy<Student>(students, "name", "Hermione") // result: Student 

美人! 現在、この関数は再利用に最適ですが、型の安全性は依然として私たちの側にあります。 ジェネリックT上記のコードスニペットの最後の行で、 Studentタイプが明示的に設定されていることに注意してください。 これは、例をできるだけ明確にするために行われますが、実際、コンパイラは必要な型を独立して導出できるため、次の例ではそのような型の改良は行いません。

これで、再利用に適した信頼できるヘルパー関数ができました。 ただし、それでも改善できます。 2番目のパラメーターを入力するときにエラーが発生し、 "name"代わりに"naem"が表示されたらどうなるでしょうか。 この関数は、探している生徒がデータベースにいないかのように動作し、最も不快なことに、エラーを生成しません。 これにより、長期のデバッグが発生する可能性があります。

このようなエラーを防ぐために、別のユニバーサルタイプPを導入しますP さらに、 PがタイプTキーであることが必要です。したがって、ここでStudent使用される場合、 Pは文字列"name""age"または"hasScar"である必要があります。 方法は次のとおりです。

 function getBy<T, P extends keyof T>(model: T[], prop: P, value): T | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "naem", "Hermione") // Error: Argument of type '"naem"' is not assignable to parameter of type '"name" | "age" | "hasScar"'. 

ジェネリックとkeyofを使用することは非常に強力なトリックです。 TypeScriptをサポートするIDEでプログラムを作成する場合、引数を入力することにより、オートコンプリート機能を利用できます。これは非常に便利です。

ただし、 getBy関数の作業はまだ完了していません。 彼女には3番目の引数があり、そのタイプはまだ設定されていません。 これは私たちにはまったく適していません。 今までは、2番目の引数として渡すものに依存するため、どのタイプであるかを事前に知ることができませんでした。 しかし、今ではタイプPがあるので、3番目の引数のタイプを動的に推測できます。 3番目の引数のタイプは、最終的にT[P]ます。 その結果、 TStudentで、 P"age"場合、 T[P]は型numberます。

 function getBy<T, P extends keyof T>(model: T[], prop: P, value: T[P]): T | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "age", "17") // Error: Argument of type '"17"' is not assignable to parameter of type 'number'. const anotherResult = getBy(students, "hasScar", "true") // Error: Argument of type '"true"' is not assignable to parameter of type 'boolean'. const yetAnotherResult = getBy(students, "name", "Harry") //      

TypeScriptでジェネリックを使用する方法について完全に理解できたと思いますが、ここで説明したコードを試して、すべてを十分に学習する場合は、 こちらをご覧ください

既存の型を拡張する


コードを変更できないインターフェイスにデータや機能を追加する必要がある場合があります。 標準オブジェクトを変更する必要があるかもしれません。たとえば、 windowオブジェクトにプロパティを追加したり、 Expressなどの外部ライブラリの動作を拡張したりします。 また、どちらの場合も、作業対象のオブジェクトに直接影響を与えることはできません。

既に知っているgetBy関数をArrayプロトタイプに追加して、同様の問題の解決策を見ていきます。 これにより、この関数を使用して、より正確な構文構造を構築できます。 現時点では、標準オブジェクトを展開することが良いか悪いかについては話していません。なぜなら、私たちの主な目標は検討中のアプローチを研究することだからです。

Arrayプロトタイプに関数を追加しようとすると、コンパイラはこれをあまり好まなくなります。

 Array.prototype.getBy = function <T, P extends keyof T>(   this: T[],   prop: P,   value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; // Error: Property 'getBy' does not exist on type 'any[]'. const bestie = students.getBy("name", "Ron"); // Error: Property 'getBy' does not exist on type 'Student[]'. const potionsTeacher = (teachers as any).getBy("subject", "Potions") //  ...   ? 

as anyコンストラクトを定期的に使用してコンパイラーを安心させようとすると、達成したすべてが無効になります。 コンパイラーは沈黙しますが、型の安全な作業を忘れることができます。

Array型を拡張することをお勧めしますが、これを行う前に、コードに同じ型の2つのインターフェイスが存在する場合にTypeScriptが状況を処理する方法について説明しましょう。 ここでは、アクションの簡単なスキームが適用されます。 可能であれば、広告は結合されます。 それらを結合できない場合、システムはエラーを出します。

したがって、このコードは機能します:

 interface Wand { length: number } interface Wand {   core: string } const myWand: Wand = { length: 11, core: "phoenix feather" } //  ! 

そして、これはそうではありません:

 interface Wand { length: number } interface Wand {   length: string } // Error: Subsequent property declarations must have the same type.  Property 'length' must be of type 'number', but here has type 'string'. 

さて、これを理解すると、かなり単純なタスクに直面していることがわかります。 つまり、必要なのは、 Array<T>インターフェイスを宣言し、 getBy関数を追加することだけです。

 interface Array<T> {  getBy<P extends keyof T>(prop: P, value: T[P]): T | null; } Array.prototype.getBy = function <T, P extends keyof T>(   this: T[],   prop: P,   value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; const bestie = students.getBy("name", "Ron"); //   ! const potionsTeacher = (teachers as any).getBy("subject", "Potions") //     

モジュールファイルに記述する可能性が高いコードのほとんどは、 Arrayインターフェイスを変更するために、グローバルスコープにアクセスする必要があることに注意してください。 これを行うには、 declare global内に型定義を配置します。 たとえば、次のように:

 declare global {   interface Array<T> {       getBy<P extends keyof T>(prop: P, value: T[P]): T | null;   } } 

外部ライブラリのインターフェースを拡張する場合、ほとんどの場合namespaceこのライブラリのnamespaceアクセスする必要があります。 以下に、 ExpressライブラリからのRequest userIdフィールドを追加する方法の例を示します。

 declare global { namespace Express {   interface Request {     userId: string;   } } } 

ここでこのセクションのコードを試すことができます

まとめ


この記事では、TypeScriptでジェネリックおよび型拡張機能を使用するための手法を検討しました。 今日学んだことが、信頼でき、理解しやすく、タイプセーフなコードを書くのに役立つことを願っています。

親愛なる読者! TypeScriptの型についてどう思いますか?

Source: https://habr.com/ru/post/J426729/


All Articles