セクション2:Propsの除外
これはセクション1で軽く触れましたが、非常に一般的な問題であるため、ここで焦点を当てます。HOCは、事前に作成されたコンポーネントにpropsを注入することがよくあります。私たちが解決したい問題は、HOCでラップされたコンポーネントが、手動で毎回HOCを再型付けすることなく、propsの縮小されたサーフェス領域を反映する型を公開することです。これにはいくつかのジェネリックが含まれますが、幸いなことにいくつかのヘルパーユーティリティを使用します。
コンポーネントがあるとします
type DogProps {
  name: string
  owner: string
}
function Dog({name, owner}: DogProps) {
  return <div> Woof: {name}, Owner: {owner}</div>
}
そして、ownerを注入するwithOwner HOCがあります
const OwnedDog = withOwner("swyx")(Dog);
withOwnerの型を、Dogのような任意のコンポーネントの型を、注入するownerプロパティを除いたOwnedDogの型に渡すようにしたいとします
typeof OwnedDog; // we want this to be equal to { name: string }
<Dog name="fido" owner="swyx" />; // this should be fine
<OwnedDog name="fido" owner="swyx" />; // this should have a typeError
<OwnedDog name="fido" />; // this should be fine
// and the HOC should be reusable for completely different prop types!
type CatProps = {
  lives: number;
  owner: string;
};
function Cat({ lives, owner }: CatProps) {
  return (
    <div>
      {" "}
      Meow: {lives}, Owner: {owner}
    </div>
  );
}
const OwnedCat = withOwner("swyx")(Cat);
<Cat lives={9} owner="swyx" />; // this should be fine
<OwnedCat lives={9} owner="swyx" />; // this should have a typeError
<OwnedCat lives={9} />; // this should be fine
では、どのようにwithOwnerの型付けをすればよいでしょうか?
- コンポーネントの型を取得します:keyof T
- マスクしたいプロパティをExcludeします:Exclude<keyof T, 'owner'>。これにより、ラップされたコンポーネントに必要なプロパティの名前のリスト(例:name)が残ります
- (オプション)除外するものが他にもある場合は、交差型を使用します:Exclude<keyof T, 'owner' | 'otherprop' | 'moreprop'>
- プロパティの名前は、関連付けられた型も持つプロパティ自体とは少し異なります。したがって、この生成された名前のリストを使用して、元のpropsからPickします:Pick<keyof T, Exclude<keyof T, 'owner'>>。これにより、新しいフィルタリングされたprops(例:{ name: string })が残ります
- (オプション)毎回これを手動で記述する代わりに、このユーティリティを使用できます:type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
- 次に、HOCをジェネリック関数として記述します
function withOwner(owner: string) {
  return function <T extends { owner: string }>(
    Component: React.ComponentType<T>
  ) {
    return function (props: Omit<T, "owner">): React.JSX.Element {
      const newProps = { ...props, owner } as T;
      return <Component {...newProps} />;
    };
  };
}
ここでは型強制が必要であることに注意してください。
これは、TypeScriptがOmit<T, "owner">と{owner: "whatever"}をマージすることがTと同じであることを認識していないためです。
詳細については、このGitHubのissueを参照してください。
ジェネリックソリューション
上記のコードスニペットを変更して、任意のpropsを注入するためのジェネリックソリューションを作成できます。
function withInjectedProps<U extends Record<string, unknown>>(
  injectedProps: U
) {
  return function <T extends U>(Component: React.ComponentType<T>) {
    return function (props: Omit<T, keyof U>): React.JSX.Element {
      //A type coercion is necessary because TypeScript doesn't know that the Omit<T, keyof U> + {...injectedProps} = T
      const newProps = { ...props, ...injectedProps } as T;
      return <Component {...newProps} />;
    };
  };
}
型強制なし
function withOwner(owner: string) {
  return function <T extends { owner: string }>(
    Component: React.ComponentType<T>
  ): React.ComponentType<Omit<T, "owner"> & { owner?: never }> {
    return function (props) {
      const newProps = { ...props, owner };
      return <Component {...newProps} />;
    };
  };
}
詳細はこちら
将来的にはここから教訓を引き出す必要がありますが、ここにそれらを示します