メインコンテンツへスキップ

セクション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の型付けをすればよいでしょうか?

  1. コンポーネントの型を取得します:keyof T
  2. マスクしたいプロパティをExcludeします:Exclude<keyof T, 'owner'>。これにより、ラップされたコンポーネントに必要なプロパティの名前のリスト(例:name)が残ります
  3. (オプション)除外するものが他にもある場合は、交差型を使用します:Exclude<keyof T, 'owner' | 'otherprop' | 'moreprop'>
  4. プロパティの名前は、関連付けられた型も持つプロパティ自体とは少し異なります。したがって、この生成された名前のリストを使用して、元のpropsからPickします:Pick<keyof T, Exclude<keyof T, 'owner'>>。これにより、新しいフィルタリングされたprops(例:{ name: string })が残ります
  5. (オプション)毎回これを手動で記述する代わりに、このユーティリティを使用できます:type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
  6. 次に、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} />;
};
};
}

(TS Playgroundへのリンク)

ここでは型強制が必要であることに注意してください。

これは、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} />;
};
};
}

(TS Playgroundへのリンク)

型強制なし

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} />;
};
};
}

(TS Playgroundへのリンク)

詳細はこちら

将来的にはここから教訓を引き出す必要がありますが、ここにそれらを示します