タイプ型がなぜ型でないのか


一見してnever タイプは、日常コーディングのために非常に役に立ちません.しかし、実際には、型の次のプロパティは非常に便利です.

[…] The never type is assignable to every type; however, no type is assignable to never (except never itself) […]


小さなハックを使用すると、既存のコードを壊したり、ランタイムのオーバーヘッドなしでコードベースの型安全性を高めるために使用できます.

問題


たぶん、あなたはあなたのコードの場所をたくさん持っています.そこでは、ユーザーID、電子メール、firstnameまたは何のようないくつかのデータを使用します.そのための標準型を使うことができます(文字列など).
getUserByUserId(userId: string): Observable<User>;
しかし、このアプローチにはいくつかの問題があります.
  • 間違ったパラメータで誤ってこのメソッドを呼ぶことができました.例えば、ユーザIDの代わりにメールで.
  • このエラーは実行時にコンパイル時ではない.
  • 時々refactoring ユーザーIDのようなフィールドを使用するすべての場所を見つけたいコードベース.これは、単純な型が非常に難しいですが、カスタムタイプを持っている場合は非常に簡単です.
  • 解決策


    組み込みの文字列型を使用しないが、ユーザーIDにカスタム型を作成する場合は、上記の問題がなくなります.
    getUserByUserId(userId: UserId): Observable<User>;
    
    今、このユーザーIDタイプが使用されるすべての場所を見つけるのは簡単です.後者は別のタイプを持っているので、誤ってメールでこのメソッドを呼び出すことはできません.

    決してタイプは遊びに来ませんか?


    この新しいユーザータイプを実装する方法?単純な解決策はうまくいきません.
    export type UserId = string;
    getUserByUserId('some string'); // no compiler errors here :-(
    
    問題は、typescriptのためですstring and UserId 互換性があります(実行時に同じであるため).我々は、変更する必要がありますUserId タイプ(コンパイラコンパイルエラーを得るために少なくとも互換性がない).私たちは新しいUserId それを達成するクラス.しかし、これは既存のコードを壊すかもしれません.しかし、助けてnever このような新しい型を定義できます.
    export type UserId = string & {
      // name of the following field needs to be unique. If it 
      // were not unique and you would reuse it for another type, 
      // both types would be compatible!
      ____doesNotMatter_UserId: never;
    };
    
    // you can't create new values of type UserId by hand, because 
    // you cannot assign anything to never. so you'll need this
    // converter function:
    export const toUserId = (userId: string) => userId as UserId;
    
    このソリューションにはいくつかの良い特性があります.
  • 文字列値を型の変数に代入することはできませんUserId 彼らが行方不明であるので、もう____doesNotMatter_UserId 属性.あなたがそれを試してみるならば、コンパイラは文句を言うでしょう.
  • 文字列を新しい型に変換するには、後者をキャストする必要があります.このタイプの値を作成する別の方法は不可能です.通常、あなたのテストでは、URLパラメータの解析中またはREST リソース.
  • 私は通常、鋳造のためのこのような工場の方法を書くconst toUserId = (userId: string) => userId as UserId; . この工場の方法は良いですので、今どこで場所を検索することができますUserId 値が作成されます.
  • 大きな利点は、このトリックがコードの実行時動作を変更しないことです.健全性チェックはコンパイル時のみ行われます.まだだからstring ランタイム中に、コード内の場所を移行するのを忘れると、何も悪いことは起こりませんstring 上の例では).
  • 文字列だけでなく、他のタイプにも使用できます.
  • では、この型を動作させましょう.
    export type UserId = string & {
      // name of the following field needs to be unique. If it 
      // were not unique and you would reuse it for another type, 
      // both types would be compatible!
      ____doesNotMatter_UserId: never;
    };
    
    // you can't create new values of type UserId, so you'll need 
    // this converter function:
    export const toUserId = (userId: string) => userId as UserId;
    
    // an example function which expects our new type
    export const isSuperUser = (userId: UserId) => 
                                       userId === 'superuser';
    
    // a function we forgot to migrate and which still get's a string
    export const unmigratedIsGuestUserFn = (userId: string) 
                                           => userId === 'guest';
    
    // some examples
    const aUserId: UserId = toUserId('u1111');
    
    isSuperUser(aUserId); // works and return false
    
    isSuperUser('superuser'); // Compile time error: Argument of type
     // 'string' is not assignable to parameter of type 'UserId'. 
     // Type 'string' is not assignable to type '{ 
     // ____doesNotMatter_UserId: never; }'.
    
    unmigratedIsGuestUserFn(aUserId); // no compile time errors 
    // because at runtime our new type is just a string :-). 
    // So this type refactoring is very safe
    
    // warning - don't reuse your ___doesNotMatter attributes:
    export type Email = string & {
      // DO NOT DO THIS:
      ____doesNotMatter_UserId: never;
    };
    
    //  Email and UserId are compatible because they have the same
    //  signature, so we don't get compiler errors here:
    const email: Email = toUserId('superuser')
    isSuperUser(email);
    
    // So -> always use unique names for these 
    // ____doesNotMatter attributes
    

    結論


    タイプの値が多くの場所で使われるが、少数の場合だけにつくられるならば、私はこのアプローチが特に役に立つとわかりました.例えば、ファクトリ関数toseridを呼び出すだけで、文字列を新しいユーザID型に変換する必要があります.
  • RESTリソースをドメインモデルに変換する(一つの場所でなければなりません)
  • URLパラメータの解析
  • テストで
    しかし、多くの場所があるかもしれません、関数はパラメタとして我々の新しいタイプを評価して、ちょうどそれをまわりに渡します.したがって、このアプローチを通して、私はより豊かなタイプモデルの前者の利点を得ます.
  • 謝辞


    私は、このトリックについてAngular Meetupは2020年にTheele Leonardによって開催された.私は別の機会には、多くのトリックが含まれてホールドを見つけたので、見ている.