反応開発者のための典型的なジェネリック


私はあなたについて知りません、しかし、私がtypescriptドキュメンテーションを読むことを試みるたびに、私は眠ります.私が脳の細胞を刺激するために良い夜の睡眠、3つのコーヒー、そして理想的にいくつかのチョコレートがあるまで、私はそれを理解しようとさえしてはならないことを私の脳に信号が書かれている方法があります.私は今、次の数ヶ月のための私の目的を発見したと思う:私はカジュアルな読者によって実際に理解できる方法で😊
多くの開発者が苦しんでいる痛みポイントの一つから始めましょう:ジェネリック!そして、ボトムアップアプローチから始めましょう:ジェネリックなしでコンポーネントを実装して、それらを必要とするときだけ、それらを導入しましょう.

イントロ
ジュディさん👩🏽‍💻. ジュディは非常に野心的な開発者であり、自分自身のオンラインショップを実装したい、ライバルにアマゾンに.彼女はそこですべてを販売します:本、映画、商品の様々な種類の1000種類以上.そして、今、彼女は、彼女がページの商品の複数のカテゴリーのために同一の探している選択の束でページを実装する必要がある段階にいます.

彼女は非常に簡単に起動しますvalue and title それらをレンダリングするonChange 選択した値が変更されたときに何かを行うことができるようにハンドラー.
import React from 'react';

type SelectOption = {
  value: string;
  label: string;
};

type SelectProps = {
  options: SelectOption[];
  onChange: (value: string) => void;
};

export const Select = ({ options, onChange }: SelectProps) => {
  return (
    <select onChange={(e) => onChange(e.target.value)}>
      {options.map((option) => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
};
これは目的のためのOKソリューションのようです.彼女はすべての彼女の製品の選択を再利用することができますし、オンラインショッピングの世界を引き継ぐ.
<>
  <Select option={bookOptions} onChange={(bookId) => doSomethingWithBooks(bookId)} />
  <Select option={movieOptions} onChange={(movieId) => doSomethingWithMovies(movieId)} />
</>
残念ながら、お店が成長するにつれて、この解決策にはいくつか問題がありました.
  • Selectコンポーネントは、特定の形式でオプションを受け入れます.すべてのコンポーネントは、消費者コンポーネントによって変換される必要があります.そして、店が成長するにつれて、より多くのページがそれを使用し始めました.そのため、コンバージョンコードはその場中で出血し始めて、維持するのが難しくなりました.
  • onChange ハンドラはid 変更された値のため、彼女は手動で変更された実際の値を見つける必要があるたびにデータの配列を手動でフィルタリングする必要がありました
  • それは完全にtypesafe、間違いを犯すのは簡単です.かつて彼女はdoSomethingWithBooks ハンドラの選択moviesOptions 間違って、それはページ全体を爆破し、事件を引き起こした.客は幸せではなかった😞

  • 💪 リファクタリング時間
    ジュディは大幅に彼女のアプリケーションを改善したいと思った
  • RAWデータの配列を通してフィルタリングするすべてのコードを取り除く
  • すべての選択オプションを生成したコードをすべて削除する
  • 選択コンポーネントの型を安全にするため、次のオプションを設定して間違ったハンドラを使用すると、型システムはそれをキャッチすることができます
  • 彼女が必要とするのは、選択コンポーネントであるということです.
  • 型指定された値の配列を受け取り、それ自身でselectオプションに変換します
  • onChange ハンドラはIDの値だけでなく、消費者側で手動で検索する必要性を取り除く
  • options and onChange 値を接続する必要があります彼女が使うならdoSomethingWithBooks 選択されたムービーを値として選択すると、それは型システムによって捕捉されたでしょう.

  • 彼女はすでにすべてのデータを入力したので、選択コンポーネントだけがいくつかの仕事を必要とした.
    export type Book = {
      id: string;
      title: string;
      author: string; // only books have it
    };
    
    export type Movie = {
      id: string;
      title: string;
      releaseDate: string; // only movies have it
    };
    ... // all other types for the shop goods
    

    強くタイプされた選択-最初の試み
    ジュディは、再び、簡単に始めた:彼女は彼女が今の本だけを受け入れる選択を実装することを決定し、その後、その後の種類の残りの部分を受け入れるように変更します.
    type BookSelectProps = {
      values: Book[];
      onChange: (value: Book) => void;
    };
    
    export const BookSelect = ({ values, onChange }: BookSelectProps) => {
      const onSelectChange = (e) => {
        const val = values.find((value) => value.id === e.target.value);
    
        if (val) onChange(val);
      };
      return (
        <select onChange={onSelectChange}>
          {values.map((value) => (
            <option key={value.id} value={value.id}>
              {value.title}
            </option>
          ))}
        </select>
      );
    };
    
    これは、すでに大きく見えました:現在、彼女はハンドラまたは値を混ぜることについて心配する必要はありません.

    今、彼女がする必要があるすべてはターンですBookSelect into GenericSelect そして、どのようにアプリケーション内のデータの残りの部分に対処するために教える.まず最初に、彼女はunion type あなたがそれらに精通していない場合は、値に-それはちょうど1のための空想的な単語ですor 型の演算子)

    しかし、それはほとんど即座に彼女にとって明らかでありました.だけでなく、彼女は手動で選択したすべてのデータ型を選択し、新しいデータ型を追加するたびに変更する必要があります.しかし、実際には、コード複雑さの観点から最悪の事態を作りました.onChange このアプローチによるコールバックvalues . それも、選択された本の著者を記録する最も明白で簡単なユースケースで、typescriptスーパー混乱を作ります:

    tは知っています、価値があることはどちらかでありえますBook or Movie , しかし、それは正確に何があるかを知りません.以降Movie 作者フィールドを持っていません、typescriptはエラーより上にコードを考慮します.
    参照example of this error in codesandbox.

    厳密に型付けされたselect -タイプスクリプトのジェネリックを使用した実際のソリューション
    そして、これは最終的にどこにgeneric types 都合がいい.ジェネリックは、一言で言えば、型のプレースホルダ以上ではありません.それはタイプスクリプトを伝える方法です:私はここでタイプがあるということを知っています、しかし、私はそれが何であるべきかについて全くわからないです、私は後であなたに話します.一般的なドキュメントの中で最も簡単な例は以下の通りです.
    function identity<Type>(a: Type): Type {
      return a;
    }
    
    つまり、ある型の引数を受け入れる関数を定義し、同じ型の値を返す関数を定義します.
    そしてその後、コード内では、このプレースホルダ型が正確に意味しているものをこの関数に伝えることができます.
    const a = identity<string>("I'm a string") // "a" will be a "string" type
    const b = identity<boolean>(false) // "b" will be a "boolean" type
    
    そして失敗したとしても失敗します.
    const a = identity<string>(false) // typescript will error here, "a" can't be boolean
    const b = identity<boolean>("I'm a string") // typescript will error here, "b" can't be string
    
    したがって、これをselectコンポーネントに適用する方法は次のようになります.

    今、私は意図的にここではコピーのペースト形式でコードを含めることはありません😅. 最初の理由は、TypeScript特有の非常に反応します:これが反応成分であるので、typescriptは非常に第一であると仮定します<Tvalue>jsx 要素と意志は失敗します.つ目の理由は、排他的なジェネリック問題ですvalue.title or value.id 私たちの選択では、この時点でのtypescriptはまだこの値を念頭に置いているかを知りません.それは私たちの値を持つことができると正当なので、どのプロパティを考えている.なぜでしょう?
    これは、このパズルの最後の部分につながる:一般的な制約.

    タイプスクリプトが少なくともいくつかの仮定をすることができるように、制約はジェネリックタイプを絞り込むのに用いられますTValue . 基本的に、それはタイプスクリプトを伝える方法ですTValue しかし、私は、それが常に少なくともid and title , それで、あなたは彼らがそこにいると仮定するために自由です.
    そして今、選択コンポーネントが完了し、完全に機能!💥 🎉 チェックアウト:
    type Base = {
      id: string;
      title: string;
    };
    
    type GenericSelectProps<TValue> = {
      values: TValue[];
      onChange: (value: TValue) => void;
    };
    
    export const GenericSelect = <TValue extends Base>({ values, onChange }: GenericSelectProps<TValue>) => {
      const onSelectChange = (e) => {
        const val = values.find((value) => value.id === e.target.value);
    
        if (val) onChange(val);
      };
    
      return (
        <select onChange={onSelectChange}>
          {values.map((value) => (
            <option key={value.id} value={value.id}>
              {value.title}
            </option>
          ))}
        </select>
      );
    };
    
    そしてjudiは最終的に彼女のアマゾンの競争相手に必要なすべての選択を実装するために使用することができます
    // This select is a "Book" type, so the value will be "Book" and only "Book"
    <GenericSelect<Book> onChange={(value) => console.log(value.author)} values={books} />
    
    // This select is a "Movie" type, so the value will be "Movie" and only "Movie"
    <GenericSelect<Movie> onChange={(value) => console.log(value.releaseDate)} values={movies} />
    
    チェックアウトfully working example in codesandbox.

    反応フックボーナスの典型的な将軍
    あなたは、最も反応フックが同様にジェネリックであるということを知っていましたか?明示的にタイプすることができますuseState or useReducer そして、あなたが定義する不幸なコピーペースト駆動の開発ミスを避けてくださいconst [book, setBook] = useState(); それから、パスmovie そこに事故で値をつけなさい.そのようなことはコードを読む次の人のために現実の少しの衝突を引き起こすかもしれませんsetBook(movie) 次のリファクタリング中.
    これはうまく動作しますが、この設定でバグを修正しようとしている人にとっては、怒りや絶望を引き起こします.
    export const AmazonCloneWithState = () => {
      const [book, setBook] = useState();
      const [movie, setMovie] = useState();
    
      return (
        <>
          <GenericSelect<Book> onChange={(value) => setMovie(value)} values={booksValues} />
          <GenericSelect<Movie> onChange={(value) => setBook(value)} values={moviesValues} />
        </>
      );
    };
    
    これはそれを防ぎます、そして、2番目の選択の値の上でsetbookを使う悪意のある試みはtypescriptによって止められます:
    export const AmazonCloneWithState = () => {
      const [book, setBook] = useState<Book | undefined>(undefined);
      const [movie, setMovie] = useState<Movie | undefined>(undefined);
    
      return (
        <>
          <GenericSelect<Book> onChange={(value) => setBook(value)} values={booksValues} />
          <GenericSelect<Movie> onChange={(value) => setMovie(value)} values={moviesValues} />
        </>
      );
    };
    
    それは今日のすべては、あなたが読んで、ジェネリックを楽しんで希望はもう謎ではない!✌🏼
    ...
    当初公開https://www.developerway.com . ウェブサイトにはこのような記事があります😉
    Subscribe to the newsletter , またはすぐに次の記事が出てくる通知を取得します.