Joiを使用したTypeScriptインターフェイスの検証

同じコードを複数回書き換えるのに2日間を費やす方法の物語。







Joi & TypeScript. A love story







エントリー



この記事では、Hapi、Joi、ルーティング、およびvalidate: { payload: ... }



詳細を省略しますvalidate: { payload: ... }



それが何であるか、用語、「インターフェース」、「タイプ」などをすでに理解していることを意味します。 ターンベースの戦略で、最も成功した戦略ではなく、これらのことについての私のトレーニングについてのみお話しします。







ちょっとした背景



現在、私はプロジェクトの唯一のバックエンド開発者です(つまり、コードを記述しています)。 機能性は本質ではありませんが、重要な本質は個人データを持つかなり長いプロファイルです。 コードの速度と品質は、ゼロからプロジェクトに独立して取り組んだ私の小さな経験に基づいており、JS(4か月目)での作業経験はさらに少なく、途中で非常に曲がって、TypeScript(以降-TS)で記述します。 日付は圧縮され、ロールは圧縮され、編集内容は常に到着します。最初にビジネスロジックコードを記述し、次にインターフェイスを記述します。 それにもかかわらず、技術的な義務は追いつき、キャップをノックすることができます。







プロジェクトで3か月作業した後、同僚の1人の辞書に切り替えて、オブジェクトのプロパティに名前を付け、どこでも同じように記述できるようにすることに最終的に同意しました。 もちろん、このビジネスの下で、私はインターフェースを書くことを引き受け、2営業日の間、それで固く立ち往生しました。







問題



単純なユーザープロファイルは抽象的な例です。









このコードのテストはすでに作成されているとしましょう。データを説明するために残っています。







 interface IUser { name: string; age: number; phone: string | number; } const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' };
      
      





まあ、すべてが明確で非常に簡単です。 私たちが思い出すように、このコードはすべて、バックエンドで、またはむしろAPIで、つまりユーザーはネットワーク経由で送られてきたデータに基づいて作成されます。 したがって、着信データを検証し、これでJoiを支援する必要があります。







 const joiUserValidator = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
      
      





「額に」ソリューションが用意されています。 このアプローチの明らかな欠点は、バリデーターがインターフェースから完全に離婚していることです。 アプリケーションの有効期間中にフィールドが変更/追加されたり、タイプが変更されたりした場合、この変更を手動で追跡し、バリデーターで示す必要があります。 何かが落ちるまで、そのような責任ある開発者はいないと思います。 また、このプロジェクトでは、アンケートは3レベルのネストの50以上のフィールドで構成されており、これを理解することは非常に困難であり、すべてを暗記することさえあります。







const joiUserValidator: IUser



指定することはできませんconst joiUserValidator: IUser



は独自のデータ型を使用するため、型Type 'NumberSchema' is not assignable to type 'number'



コンパイル時にエラーが生成されるType 'NumberSchema' is not assignable to type 'number'



。 しかし、インターフェイスで検証を実行する方法が必要ですか?







おそらく私はそれを正しくグーグルしなかったか、答えをよく研究していませんでしたが、すべての決定は次のようにextractTypes



とある種の激しい自転車にextractTypes



られました:







 type ValidatedValueType<T extends joi.Schema> = T extends joi.StringSchema ? string : T extends joi.NumberSchema ? number : T extends joi.BooleanSchema ? boolean : T extends joi.ObjectSchema ? ValidatedObjectType<T> : /* ... more schemata ... */ never;
      
      





解決策



サードパーティのライブラリを使用する



なぜだ。 私の仕事について人々に尋ねたとき、私は答えの1つで、そして後で、コメントで( keenondrumsのおかげで)これらのライブラリへのリンクを受け取りました

https://github.com/typestack/class-validator

https://github.com/typestack/class-transformer







しかし、自分でそれを理解し、TSの作業をよりよく理解することに関心があり、問題を一瞬で解決することを促すものはありませんでした。







すべてのプロパティを取得



静的に関する以前の研究はなかったため、上記のコードは、型で三項演算子を使用するという点でアメリカを発見しました。 幸いなことに、プロジェクトに適用することはできませんでした。 しかし、私は別の興味深い自転車を見つけました:







 interface IUser { name: string; age: number; phone: string | number; } type UserKeys<T> = { [key in keyof T]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
      
      





かなりトリッキーでミステリアスな条件下でTypeScript



を使用すると、たとえば、インターフェイスからキーを取得できます。ただし、通常のJSオブジェクトであるかのように、 type



構造key in keyof T



およびジェネリックのみkey in keyof T



ます。 UserKeys



タイプの結果として、インターフェイスを実装するすべてのオブジェクトには同じプロパティセットが必要ですが、値のタイプは任意です。 これにはIDEのヒントが含まれますが、値のタイプを明確に示すものではありません。







私が使用できなかった別の興味深いケースがあります。 なぜあなたはこれが必要なのか教えてくれるかもしれません(私は部分的に推測しますが、十分な応用例はありません):







 interface IUser { name: string; age: number; phone: string | number; } interface IUserJoi { name: Joi.StringSchema, age: Joi.NumberSchema, phone: Joi.AlternativesSchema } type UserKeys<T> = { [key in keyof T]: T[key]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const userJoiValidator: UserKeys<IUserJoi> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
      
      





変数タイプを使用する



タイプを明示的に設定し、「OR」を使用してプロパティを抽出すると、ローカルで機能するコードを取得できます。







 type TString = string | Joi.StringSchema; type TNumber = number | Joi.NumberSchema; type TStdAlter = TString | TNumber; type TAlter = TStdAlter | Joi.AlternativesSchema; export interface IUser { name: TString; age: TNumber; phone: TAlter; } type UserKeys<T> = { [key in keyof T]; } const olex: UserKeys<IUser> = { name: 'Olex', age: 67, phone: '79998887766' }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
      
      





このコードの問題は、たとえばデータベースから有効なオブジェクトを取得する場合に発生します。つまり、TSは単純なデータまたはJoiデータのタイプを事前に知りません。 これにより、 number



として予期されるフィールドで数学演算を実行しようとすると、エラーが発生する場合があります。







 const someUser: IUser = getUserFromDB({ name: 'Aleg' }); const someWeirdMath = someUser.age % 10; // error TS2362: The left-hand side of an arithmetic operation must be of type'any', 'number', 'bigint' or an enum type
      
      





このエラーはJoi.NumberSchema



から発生します。年齢はnumber



だけではないためです。 彼らが戦ったものと出会ったもの。







2つのソリューションを1つに組み合わせますか?



この時点のどこかで、営業日は論理的な結論に近づいていました。 私は一息ついてコーヒーを飲み、このポルノから地獄を消した。 これらのインターネットをあまり読む必要はありません! 時が来た 散弾銃を取り、 洗脳:







  1. オブジェクトは、明示的な値型で形成する必要があります。
  2. ジェネリックを使用して、型を1つのインターフェイスにスローできます。
  3. ジェネリックはデフォルトのタイプをサポートしています。
  4. type



    構造は、明らかに他の何かを実行できます。


デフォルトのタイプで汎用インターフェースを作成します。







 interface IUser < TName = string, TAge = number, TAlt = string | number > { name: TName; age: TAge; phone: TAlt; }
      
      





Joiの場合、メインインターフェイスを次のように継承して、2番目のインターフェイスを作成できます。







 interface IUserJoi extends IUser < Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema > {}
      
      





次の開発者は、 IUserJoi



を軽いハートで展開することもできるし、さらに悪いこともあるため、十分ではありません。 より制限されたオプションは、同様の動作を取得することです:







 type IUserJoi = IUser<Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema>;
      
      





私達は試みます:







 const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' }; const joiUser: IUserJoi = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
      
      





UPD:

Joi.object



には、エラーTS2345



と戦わなければなりas any



でした。 上記のオブジェクトはまだインターフェイス上にあるため、これは重要な仮定ではないと思います。







 const joiUserInfo = { info: Joi.object(joiUser as any).required() };
      
      





コンパイルされ、使用場所がきれいに見え、特別な条件がない場合は常にデフォルトのタイプが設定されます! 美しさ...

-

...私が2営業日費やしたこと







まとめ



このすべてからどのような結論を引き出すことができますか:







  1. 明らかに、質問に対する答えを見つける方法を学びませんでした。 確かに、リクエストが成功した場合、このソリューション(またはそれ以上)は、検索エンジンの最初の5kリンクにあります。
  2. 動的なものから静的なものへの切り替えはそれほど簡単ではありません。多くの場合、私はそのような群れを叩きます。
  3. ジェネリックはクールです。 Habrとstackoverflowはいっぱいです 自転車の 強い型付けを構築するための非自明なソリューション...実行時以外。


私たちが獲得したもの:







  1. インターフェイスを変更すると、バリデータを含むすべてのコードが落ちます。
  2. エディターでは、バリデーターを記述するためのプロパティ名とオブジェクト値のタイプに関するヒントが表示されました。
  3. 同じ目的のための曖昧なサードパーティライブラリの欠如;
  4. Joiルールは、必要な場合にのみ適用されます。その他の場合は、デフォルトタイプ。
  5. 誰かがプロパティの値型を変更したい場合は、コードの正しい編成を使用して、このプロパティに関連付けられたすべての型が集められた場所に行きます。
  6. type



    抽象化の背後にあるジェネリックを美しくシンプルに隠し、monstruson構造から視覚的にコードをアンロードすることを学びました。


道徳:経験は貴重です;残りには世界地図があります。







最終結果を表示、タッチ、実行できます:

https://repl.it/@Melodyn/Joi-by-interface








All Articles