反応する開発者のための上級タイプスクリプト-パート3



これは、“反応の開発者のための高度なタイプスクリプト”シリーズの3番目の記事です.前の章では野心的な開発者ジュディと一緒にどのように、理由を考え出したTypescript generics 再利用可能な反応コンポーネントを作成するのに便利ですtype guards, keyof, typeof, is, as const and indexed types . JADIをアマゾンに競合させながら実装しながら、商品のさまざまなカテゴリーと選択したコンポーネントを選択する能力を持つオンラインウェブサイトを実装しました.今は再びシステムを改善し、プロセスの中で何が目的であるかを学ぶ時間ですexhaustiveness checking , タイプの狭まりがどのように働くかtypescript enums が役に立つ.
あなたが私たちがで始めている例のコードを見ることができますthis codesandbox .

決して徹底しないチェック
どのように我々はカテゴリをタブを実装覚えてみましょう.文字列の配列がswitch すべてのタブで選択コンポーネント、およびカテゴリの選択コンポーネントを返します.
const tabs = ["Books", "Movies", "Laptops"] as const;
type Tabs = typeof tabs;
type Tab = Tabs[number];

const getSelect = (tab: Tab) => {
  switch (tab) {
    case "Books":
      return (
        <GenericSelect<Book> ... />
      );
    case "Movies":
      return (
        <GenericSelect<Movie> ... />
      );
    case "Laptops":
      return (
        <GenericSelect<Laptop> ... />
      );
  }
};

export const TabsComponent = () => {
  const [tab, setTab] = useState<Tab>(tabs[0]);

  const select = getSelect(tab);

  return (
    <>
      Select category:
      <GenericSelect<Tab>
        onChange={(value) => setTab(value)}
        values={tabs}
        formatLabel={formatLabel}
      />
      {select}
    </>
  );
};
すべてが完全に入力されるので、typoがどこでも起こるならば、それはtypescriptによって拾われます.しかし、それは完全にタイプされますか?リストに新しいカテゴリを追加したい場合はどうなりますかPhones ? 十分に簡単に思えます:私はちょうど配列とスイッチ声明にそれを加えます.
const tabs = ["Books", "Movies", "Laptops", "Phones"] as const;

const getSelect = (tab: Tab) => {
  switch (tab) {
    // ...
    case "Phones":
      return (
        <GenericSelect<Phone> ... />
      );
  }
};
そして、このような単純な実装では、多くのトラブルをもたらさないでしょう.しかし、実際の生活では、このコードが分離され、離れて抽象化され、実装の層の背後に隠れている可能性が高い.それから、私はちょうど配列に電話を加えるならば、何が起こるでしょうか、スイッチケースを忘れます?
const tabs = ["Books", "Movies", "Laptops", "Phones"] as const;

const getSelect = (tab: Tab) => {
  switch (tab) {
    case "Books":
      // ...
    case "Movies":
      // ...
    case "Laptops":
      // ...
  }
};
この実装で-何も良い、残念ながら.Typescriptはそれで完全に罰金です、バグは手動テストの間、見逃されるかもしれません、そして、それは生産に行きます、そして、顧客がメニューで「電話」を選ぶとき、彼らはスクリーンで何も見ません.
しかしながら、それはこれのようである必要はありません.我々がオペレーターを使うときif or switch TypeScriptは「ナローイング」と呼ばれるものを実行します.つまり、すべての文でユニオン型の使用可能なオプションを減らします.例えば、「本」だけのスイッチケースを持っているならば、「本」タイプは最初に削除されますcase しかし、残りは以下のようになります:
const tabs = ["Books", "Movies", "Laptops"] as const;

// Just "Books" in the switch statement
const getSelect = (tab: Tab) => {
  switch (tab) {
    case "Books":
      // tab's type is Books here, it will not be available in the next cases
      return <GenericSelect<Book> ... />
    default:
      // at this point tab can be only "Movies" or "Laptops"
      // Books have been eliminated at the previous step
  }
};
すべての可能な値を使用すると、typescriptはnever 種類
const tabs = ["Books", "Movies", "Laptops"] as const;

const getSelect = (tab: Tab) => {
  switch (tab) {
    case "Books":
      // "Books" have been eliminated here
    case "Movies":
      // "Movies" have been eliminated here
    case "Laptops":
      // "Laptops" have been eliminated here
    default:
      // all the values have been eliminated in the previous steps
      // this state can never happen
      // tab will be `never` type here
  }
};
そして、このトリックのために非常に慎重に手を見てくださいnever 種類そして、何らかの理由で、それは実際に不可能ではありません(すなわち、我々は配列に「電話」を加えました、しかし、そうではありません.」switch - TypeScriptは失敗します!
// Added "Phones" here, but not in the switch
const tabs = ["Books", "Movies", "Laptops", "Phones"] as const;

// Telling typescript explicitly that we want tab to be "never" type
// When this function is called, it should be "never" and only "never"
const confirmImpossibleState = (tab: never) => {
  throw new Error(`Reacing an impossible state because of ${tab}`);
};

const getSelect = (tab: Tab) => {
  switch (tab) {
    case "Books":
      // "Books" have been eliminated
    case "Movies":
      // "Movies" have been eliminated
    case "Laptops":
      // "Laptops" have been eliminated
    default:
      // This should be "impossible" state,
      // but we forgot to add "Phones" as one of the cases
      // and "tab" can still be the type "Phones" at this stage.

      // Fortunately, in this function we assuming tab is always "never" type
      // But since we forgot to eliminate Phones, typescript now will fail!
      confirmImpossibleState(tab);
  }
};
今実装は完璧です!任意のtyposはtypescriptによってピックアップされ、非既存のカテゴリがピックアップされ、カテゴリを逃したもピックアップされます!このトリックはExhaustiveness checking ところで.

決して徹底しないチェック
興味深いことに、徹底的なトリックが働くためには、実際には必要ありませんnever タイプと「不可能」状態.あなたが必要とするすべては、ナローイングと除去のこのプロセスを理解するだけであり、最後のステップで目的の型を「ロック」する方法です.
覚えて、我々は我々のformatLabel selectコンポーネントに渡す関数で、値型に基づいて選択オプションの文字列を返します.
export type DataTypes = Book | Movie | Laptop | string;

export const formatLabel = (value: DataTypes) => {
  if (isBook(value)) return `${value.title}: ${value.author}`;
  if (isMovie(value)) return `${value.title}: ${value.releaseDate}`;
  if (isLaptop(value)) return value.model;

  return value;
};
全く同じバグのもう一つの完全な候補Phone データ型の一つとして、実際のチェックを忘れますか?現在の実装で-何も良い再び、電話の選択オプションが壊れてしまいます.しかし、私たちが徹底的な知識を機能に適用するならば、我々はこうすることができます:
export type DataTypes = Book | Movie | Laptop | Phone | string;

 // When this function is called the value should be only string
 const valueShouldBeString = (value: string) => value;

 const formatLabel = (value: DataTypes) => {
  // we're eliminating Book type from the union here
  if (isBook(value)) return `${value.title}: ${value.author}`;

  // here value can only be Movie, Laptop, Phone or string

  // we're eliminating Movie type from the union here
  if (isMovie(value)) return `${value.title}: ${value.releaseDate}`;

  // here value can only be Laptop, Phone or string

  // we're eliminating Laptop type from the union here
  if (isLaptop(value)) return value.model;

  // here value can only be Phone or string

  // But we actually want it to be only string
  // And make typescript fail if it is not
  // So we just call this function, that explicitly assigns "string" to value

  return valueShouldBeString(value);

  // Now, if at this step not all possibilities are eliminated
  // and value can be something else other than string (like Phone in our case)
  // typescript will pick it up and fail!
};
我々は、すべての可能なユニオンタイプを除いたstring , そして、最後のステップの「ロックされた」ストリング.きれいなきちんとした、huh?
See fully working example in this codesandbox.

ENUMによるコード可読性の改善
現在、それは我々のカテゴリー実現であるTypescript芸術のこの美しい部分の最終的なポーランドのための時間です.私はあなたのことを知らないが、この部分は少し心配している.
const tabs = ["Books", "Movies", "Laptops"] as const;
type Tabs = typeof tabs;
type Tab = Tabs[number];
私はそれのような構造を見ているたびに、それはちょうど少し私の脳を壊す、それ自体は間違っています.それは常に正確にここで起こっているかを理解するために、1つの2つの追加秒かかります.幸いにも、同じ問題に苦しむ人々のためにそれを改善する方法があります.あなたは、タイプスクリプトが支持するのを知っていましたかenums ? これにより、名前付き定数の集合を定義することができます.そして、それらの最良の部分-それらはGET Goから強くタイプされます、そして、あなたは文字通り、同じようにタイプと値として同じenumを使うことができます.🤯
基本的には
const tabs = ["Books", "Movies", "Laptops"] as const;
type Tabs = typeof tabs;
type Tab = Tabs[number];
これに置き換えられる可能性があります.
enum Tabs {
  'MOVIES' = 'Movies',
  'BOOKS' = 'Books',
  'LAPTOPS' = 'Laptops',
}
次に、特定の値にアクセスする必要がある場合は、オブジェクトのようにドット表記を使用します.
const movieTab = Tabs.MOVIES; // movieTab will be `Movies`
const bookTab = Tabs.BOOKS; // bookTab will be `Books`
とちょうど使用Tabs あなたがタイプとしてenumを参照したいとき!
タブコードを見ると、enumタブと全てのタブ文字列をenumの値で置き換えることができます.

また、タブコンポーネントの実際の実装では、同じです.型を置換し、値を置換し、配列の形式でENUMの値を選択します.

See the full code example in this codesandbox.
完璧!😍 😎
それは今日のすべてです、あなたが読書を楽しんで、そして、現在、少しより少しTypeScriptの狭くなっている、徹底的なチェックとenumに自信があることを願っています.次回参照😉
...
当初公開https://www.developerway.com . ウェブサイトにはこのような記事があります😉
Subscribe to the newsletter , またはすぐに次の記事が出てくる通知を取得します.