跳到主要内容

第 2 节:排除 Props

这在第 1 节中传递时已涵盖,但我们在此重点介绍它,因为它是一个非常常见的问题。HOC 经常将 props 注入预制组件。我们想要解决的问题是让 HOC 包装的组件公开一个反映 props 减少的表面积的类型 - 无需每次都手动重新键入 HOC。这涉及一些泛型,幸运的是,有一些辅助实用程序。

假设我们有一个组件

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` 的组件的类型传递到 `OwnedDog` 的类型中,减去它注入的 `owner` 属性

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 问题。

通用解决方案

上面的代码片段可以修改为创建一个通用的解决方案来注入任何任意 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 链接)

了解更多

我们将来需要从这里吸取教训,但这里有一些