セクション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} />;
};
};
}
詳細はこちら
将来的にはここから教訓を引き出す必要がありますが、ここにそれらを示します