作者:Ohans Emmanuel✏️

在这份详细(和解释性)的指南中,我将讨论如何使用 TypeScript 构建强类型的多态 React 组件。我们将介绍以下部分:

  • 多态组件的真实示例

  • *Chakra UI 的as道具

*MUI 的component道具

  • 构建简单的多态组件

  • 这个简单实现的问题

*as属性可以接收无效的 HTML 元素

*可以为有效元素传递错误的属性

*无属性支持!

*为什么这样不好?

  • 如何使用 TypeScript 在 React 中构建强类型多态组件

*确保as属性只接收有效的 HTML 元素字符串

*在泛型声明后加逗号

*约束泛型

  • 使用 TypeScript 泛型处理有效的组件属性

  • 处理默认值as属性

  • 使用其道具使组件可重用

  • 严格省略通用组件道具

  • 为多态类型创建可重用实用程序

  • 支持多态组件中的引用

如您所见,这很长,因此请随意跳过。如果您想继续关注,请在我的 GitHub 上加注官方代码库以供参考。

多态组件的真实示例

您已经使用过多态组件的可能性非零。开源组件库通常实现某种多态组件。

让我们考虑一些您可能熟悉的:Chakra UIas道具和 MUIcomponent道具。

Chakra UI 的as道具

[脉轮 UI](https://res.cloudinary.com/practicaldev/image/fetch/s--VjWjVfTt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.logrocket. com/wp-content/uploads/2022/05/chakra-ui.jpeg)Chakra UI如何实现多态道具?答案是暴露一个as道具。as属性被传递给组件以确定它最终应该呈现的容器元素。Chakra UI As Prop 使用as道具所需要做的就是将它传递给组件,在这种情况下是Box:

<Box as='button' borderRadius='md' bg='tomato' color='white' px={4} h={8}>
  Button
</Box>

进入全屏模式 退出全屏模式

现在,该组件将呈现一个button元素。[显示为按钮的框组件](https://res.cloudinary.com/practicaldev/image/fetch/s--_y1k34lJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https:/ /blog.logrocket.com/wp-content/uploads/2022/05/box-component-rendered-as-button.png)如果您将as属性更改为h1:

<Box as="h1"> Hello </Box>

进入全屏模式 退出全屏模式

现在,Box组件渲染了一个h1:[The Box Component Rendered As An H1](https://res.cloudinary.com/practicaldev/image/fetch/s--5Gu5jnG1--/c_limit%2Cf_auto% 2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.logrocket.com/wp-content/uploads/2022/05/box-component-rendered-as-h1.png)这是一个多态组件在工作!它可以被渲染成完全独特的元素,所有这些都通过传递一个道具来实现。

MUI 的component道具

[MUI's Component Prop](https://res.cloudinary.com/practicaldev/image/fetch/s--Kc9roSbp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.logrocket .com/wp-content/uploads/2022/05/MUI-component-prop.jpeg) 与 Chakra UI 类似,MUI允许称为component的多态 prop,其实现方式类似:您将其传递给组件并声明您要呈现的元素或自定义组件。这是来自官方文档的示例:MUI 组件道具

<List component="nav">
  <ListItem button>
    <ListItemText primary="Trash" />
  </ListItem>
</List>

进入全屏模式 退出全屏模式

List被传递了一个nav的组件属性;当它被渲染时,它将渲染一个nav容器元素。

另一个用户可以使用相同的组件,但不能用于导航;相反,他们可能想要呈现一个待办事项列表:

<List component="ol">
  ...
</List>

进入全屏模式 退出全屏模式

在这种情况下,List将呈现一个有序列表ol元素。

谈论灵活性!有关多态组件,请参阅用例](https://github.com/ohansemmanuel/polymorphic-react-component/blob/master/use-cases.pdf)的[摘要。

正如您将在本文的以下部分中看到的那样,多态组件非常强大。除了接受元素类型的道具之外,它们还可以接受自定义组件作为道具。

这将在本文的下一节中讨论。现在,让我们开始构建我们的第一个多态组件!

构建简单的多态组件

与您的想法相反,构建您的第一个多态组件非常简单。这是一个基本的实现:

const MyComponent = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

进入全屏模式 退出全屏模式

请注意,多态道具as类似于 Chakra UI。这是我们为控制多态组件的渲染元素而公开的道具。

其次,请注意as道具不是直接渲染的。以下将是错误的:

const MyComponent = ({ as, children }) => {
  // wrong render below 👇 
  return <as>{children}</as>;
};

进入全屏模式 退出全屏模式

当在运行时渲染一个元素类型时,必须先将它赋给一个大写的变量,然后再渲染大写的变量。[使用大写变量](https://res.cloudinary.com/practicaldev/image/fetch/s--Jw_5LCUx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog. logrocket.com/wp-content/uploads/2022/05/using-capitalized-variable.png)现在您可以继续使用此组件,如下所示:

<MyComponent as="button">Hello Polymorphic!<MyComponent>
<MyComponent as="div">Hello Polymorphic!</MyComponent>
<MyComponent as="span">Hello Polymorphic!</MyComponent>
<MyComponent as="em">Hello Polymorphic!</MyComponent>

进入全屏模式 退出全屏模式

请注意,不同的as道具被传递给上面的渲染组件。[不同的渲染元素](https://res.cloudinary.com/practicaldev/image/fetch/s--aBCfOFNL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog. logrocket.com/wp-content/uploads/2022/05/different-rendered-elements.jpeg)

这个简单实现的问题

上一节的实现虽然很标准,但也有很多缺点。让我们来探索其中的一些。

1\。as属性可以接收无效的 HTML 元素

目前,用户可以编写以下内容:

<MyComponent as="emmanuel">Hello Wrong Element</MyComponent>

进入全屏模式 退出全屏模式

这里传递的as属性是emmanuel。Emmanuel 显然是一个错误的 HTML 元素,但是浏览器也尝试渲染这个元素。[呈现错误的 HTML 元素类型](https://res.cloudinary.com/practicaldev/image/fetch/s--zm8qjrWH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https:// blog.logrocket.com/wp-content/uploads/2022/05/rendering-a-wrong-html-element-type.png)理想的开发体验是在开发过程中显示某种错误。例如,用户可能会输入一个简单的拼写错误——divv而不是div——并且不会得到任何错误提示。

2\。可以为有效元素传递错误的属性

考虑以下组件用法:

<MyComponent as="span" href="https://www.google.com">
   Hello Wrong Attribute
</MyComponent>

进入全屏模式 退出全屏模式

消费者可以将span元素传递给as道具,也可以传递href道具。

这在技术上是无效的。span元素不(也不应该)采用href属性。那是无效的 HTML 语法。但是,我们构建的组件的使用者仍然可以继续编写此代码,并且在开发过程中不会出错。

3\。无属性支持!

再次考虑简单的实现:

const MyComponent = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

进入全屏模式 退出全屏模式

该组件接受的唯一道具是aschildren,仅此而已。即使是有效的as元素道具也没有属性支持,即,如果as是锚元素a,我们也应该支持将href传递给组件。

<MyComponent as="a" href="...">A link </MyComponent>

进入全屏模式 退出全屏模式

即使在上面的示例中传递了href,组件实现也不会收到其他道具。只有aschildren被解构。

您最初的想法可能是继续传播传递给组件的所有其他道具,如下所示:

const MyComponent = ({ as, children, ...rest }) => {
  const Component = as || "span";

  return <Component {...rest}>{children}</Component>;
};

进入全屏模式 退出全屏模式

这似乎是一个不错的解决方案,但现在它突出了上面提到的第二个问题。错误的属性现在也将传递给组件。

考虑以下:

<MyComponent as="span" href="https://www.google.com">
   Hello Wrong Attribute
</MyComponent>

进入全屏模式 退出全屏模式

并注意最终呈现的标记:[具有 Href 属性的 Span 元素](https://res.cloudinary.com/practicaldev/image/fetch/s--FwaC7DLx--/c_limit%2Cf_auto%2Cfl_progressive% 2Cq_auto%2Cw_880/https://blog.logrocket.com/wp-content/uploads/2022/05/span-element-with-href-attribute.png) 带有hrefspan是无效的 HTML。

为什么这样不好?

回顾一下,我们简单实现的当前问题是低于标准的,因为:

  • 它提供了糟糕的开发者体验

  • 它不是类型安全的。错误可以(并且将会)潜入

我们如何解决这些担忧?需要明确的是,这里没有魔杖可以挥动。但是,我们将利用 TypeScript 来确保您构建强类型的多态组件。

完成后,使用您的组件的开发人员将避免上述运行时错误,而是在开发或构建期间捕获它们——这一切都归功于 TypeScript。

如何使用 TypeScript 在 React 中构建强类型多态组件

如果你正在阅读这篇文章,一个先决条件是你已经了解了一些 TypeScript——至少是基础知识。如果你不知道 TypeScript 是什么,我强烈推荐给这个文档先阅读。

在本节中,我们将使用 TypeScript 解决上述问题并构建强类型多态组件。我们将开始的前两个要求包括:

  • as道具不应接收无效的 HTML 元素字符串

  • 不应该为有效元素传递错误的属性

在下一节中,我们将介绍 TypeScript 泛型,以使我们的解决方案更加健壮、对开发人员友好且具有生产价值。

确保as属性只接收有效的 HTML 元素字符串

这是我们目前的解决方案:

const MyComponent = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

进入全屏模式 退出全屏模式

为了使本指南的下一部分变得实用,我们将组件的名称从MyComponent更改为Text,并假设我们正在构建一个多态的Text组件。

const Text = ({ as, children }) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

进入全屏模式 退出全屏模式

现在,根据您对泛型的了解,很明显我们最好用泛型类型表示as,即基于用户传入的变量类型。[As Prop](https:/ /res.cloudinary.com/practicaldev/image/fetch/s--SFGyLLI3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.logrocket.com/wp-content/uploads/2022/05/ as-prop.png) 让我们继续迈出第一步,如下所示:

export const Text = <C>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

进入全屏模式 退出全屏模式

注意泛型C是如何定义的,然后在 propas的类型定义中传递。

但是,如果您编写了这个看似完美的代码,您将让 TypeScript 用比您想要的更多的波浪形红线大喊出许多错误 🤷u200d♀️[JSX 通用错误](https://res.cloudinary .com/practicaldev/image/fetch/s--iLYgAHDe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.logrocket.com/wp-content/uploads/2022/05/jsx-generic- error.png) 这里发生的是语法中的一个缺陷,用于泛型在.tsx文件中。有两种方法可以解决这个问题。

1\。在泛型声明后添加逗号

这是声明多个泛型的语法。一旦你这样做了,TypeScript 编译器就会清楚地理解你的意图,并且错误就会被消除。

// note the comma after "C" below 👇
export const Text = <C,>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

进入全屏模式 退出全屏模式

2\。约束泛型

第二种选择是在您认为合适的情况下限制泛型。对于初学者,您可以使用unknown类型,如下所示:

// note the extends keyword below 👇
export const Text = <C extends unknown>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

进入全屏模式 退出全屏模式

现在,我将坚持第二种解决方案,因为它更接近我们的最终解决方案。不过,在大多数情况下,我使用多个通用语法并仅添加一个逗号。

但是,使用我们当前的解决方案,我们会遇到另一个 TypeScript 错误:

JSX 元素类型“组件”没有任何构造或调用签名。 ts(2604)

[无构造或调用错误](https://res.cloudinary.com/practicaldev/image/fetch/s--HMBn74JK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog .logrocket.com/wp-content/uploads/2022/05/no-construct-or-call-error.png)

这类似于我们在使用echoLength函数时遇到的错误。就像访问未知变量类型的length属性一样,这里也可以这样说:试图将任何泛型类型渲染为有效的 React 组件是没有意义的。

我们只需要约束泛型以适应有效 React 元素类型的模型。

为了实现这一点,我们将利用内部 React 类型:React.ElementType,并确保泛型被限制为适合该类型:

// look just after the extends keyword 👇
export const Text = <C extends React.ElementType>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

进入全屏模式 退出全屏模式

请注意,如果您使用的是旧版本的 React,则可能需要导入更新的 React 版本,如下所示:

import React from 'react'

进入全屏模式 退出全屏模式

有了这个,我们就没有更多的错误了!

现在,如果您继续按如下方式使用此组件,它将正常工作:

<Text as="div">Hello Text world</Text>

进入全屏模式 退出全屏模式

但是,如果你传递了一个无效的as属性,你现在会得到一个适当的 TypeScript 错误。考虑下面的例子:

<Text as="emmanuel">Hello Text world</Text>

进入全屏模式 退出全屏模式

并抛出错误:

类型 '"emmanuel"' 不可分配给类型 'ElementType |不明确的'。

[类型 Emmanuel 不可分配错误](https://res.cloudinary.com/practicaldev/image/fetch/s--ve3Oxhyk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https:// blog.logrocket.com/wp-content/uploads/2022/05/type-emmanuel-not-assginable-error.jpeg)这太棒了!我们现在有一个解决方案,它不接受as道具的乱码,并且还可以防止令人讨厌的拼写错误,例如divv而不是div

这是一个更好的开发者体验!

使用 TypeScript 泛型处理有效的组件属性

在解决第二个用例时,您将体会到泛型的真正强大之处。首先,让我们了解我们在这里要完成的工作。

一旦我们收到一个通用的as类型,我们希望确保传递给我们组件的剩余道具是相关的,基于as道具。

因此,例如,如果用户传入asas道具,我们希望href同样是有效的道具![作为道具和 Href](https://res.cloudinary.com/practicaldev/image/fetch/s--0Amrqy4W--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog. logrocket.com/wp-content/uploads/2022/05/as-prop-and-href.png)为了让您了解我们将如何完成此任务,请查看我们解决方案的当前状态:

export const Text = <C extends React.ElementType>({
  as,
  children,
}: {
  as?: C;
  children: React.ReactNode;
}) => {
  const Component = as || "span";

  return <Component>{children}</Component>;
};

进入全屏模式 退出全屏模式

该组件的 prop 现在由对象类型表示:

{
  as?: C;
  children: React.ReactNode;
}

进入全屏模式 退出全屏模式

在伪代码中,我们想要的是以下内容:

{
  as?: C;
  children: React.ReactNode;
} & {
  ...otherValidPropsBasedOnTheValueOfAs
}

进入全屏模式 退出全屏模式

[伪代码](https://res.cloudinary.com/practicaldev/image/fetch/s--eOYdEmfy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.logrocket.com /wp-content/uploads/2022/05/pseudocode.png)

这一要求足以让一个人抓住稻草。我们不可能编写一个函数来根据as的值确定合适的类型,手动列出联合类型也不明智。

那么,如果有一个来自React的提供类型充当“函数”,它将根据您传递的内容返回有效的元素类型怎么办?

在介绍解决方案之前,让我们进行一些重构。让我们将组件的 props 拉出到一个单独的类型中:

// 👇 See TextProps pulled out below 
type TextProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} 

export const Text = <C extends React.ElementType>({
  as,
  children,
}: TextProps<C>) => { // 👈 see TextProps used 
  const Component = as || "span";
  return <Component>{children}</Component>;
};

进入全屏模式 退出全屏模式

这里重要的是注意泛型是如何传递给TextProps<C>的。类似于 JavaScript 中的函数调用——但使用尖括号。

这里的魔杖是利用React.ComponentPropsWithoutRef类型,如下所示:

type TextProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>; // 👈 look here 

export const Text = <C extends React.ElementType>({
  as,
  children,
}: TextProps<C>) => {
  const Component = as || "span";
  return <Component>{children}</Component>;
};

进入全屏模式 退出全屏模式

请注意,我们在这里引入了一个交集。本质上,我们说TextProps的类型是包含aschildren和其他一些由React.ComponentPropsWithoutRef表示的类型的对象类型。[React.ComponentPropsWithoutRef](https://res.cloudinary.com/practicaldev/ image/fetch/s--dvqLsvKt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.logrocket.com/wp-content/uploads/2022/05/react-componentpropswithoutref.png)

如果您阅读代码,可能会很明显这里发生了什么。

基于由通用C表示的as的类型,React.componentPropsWithoutRef将返回与传递给as属性的字符串属性相关的有效组件属性。

还有一点需要注意。[不同的 ComponentProps 类型变体](https://res.cloudinary.com/practicaldev/image/fetch/s--SJV_Ryj6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog. logrocket.com/wp-content/uploads/2022/05/different-componentprops-type-variants.png)

如果您刚开始输入并依赖编辑器中的 IntelliSense,您会意识到React.ComponentProps...类型有三种变体:

1.React.ComponentProps

2.React.ComponentPropsWithRef

3.React.ComponentPropsWithoutRef

如果您尝试使用第一个ComponentProps,您会看到相关说明:

PreferComponentPropsWithRef,如果ref被转发,或者ComponentPropsWithoutRef不支持 refs。

[注意首选 ComponentPropsWithRef 或 ComponentPropsWithoutRef](https://res.cloudinary.com/practicaldev/image/fetch/s--EP14EmtF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https:// blog.logrocket.com/wp-content/uploads/2022/05/prefer-componentpropswithref-or-without.png)

这正是我们所做的。现在,我们将忽略支持ref道具的用例并坚持使用ComponentPropsWithoutRef

现在,让我们尝试一下解决方案!

如果您继续错误地使用此组件,例如,将有效的as道具与其他不兼容的道具一起传递,您将收到错误消息。

<Text as="div" href="www.google.com">Hello Text world</Text>

进入全屏模式 退出全屏模式

div的值对as属性完全有效,但div不应具有href属性。

这是错误的,TypeScript 正确捕获了错误:Property 'href' does not exist on type ....[Property Href 不存在错误](https://res.cloudinary.com/practicaldev/image/fetch/s--R9Pe2ddV--/ c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.logrocket.com/wp-content/uploads/2022/05/property-href-doesnt-exist-error.png)

这很棒!我们有一个更好、更强大的解决方案。

最后,确保将其他道具向下传递到渲染元素:

type TextProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>; 

export const Text = <C extends React.ElementType>({
  as,
  children,
  ...restProps, // 👈 look here
}: TextProps<C>) => {
  const Component = as || "span";

  // see restProps passed 👇
  return <Component {...restProps}>{children}</Component>;
};

进入全屏模式 退出全屏模式

我们继续吧。

处理默认as属性

再次考虑我们当前的解决方案:

export const Text = <C extends React.ElementType>({
  as,
  children,
  ...restProps
}: TextProps<C>) => {
  const Component = as || "span"; // 👈 look here

  return <Component {...restProps}>{children}</Component>;
};

进入全屏模式 退出全屏模式

特别是,如果省略as属性,请注意提供默认元素的位置。

const Component = as || "span"

进入全屏模式 退出全屏模式

这在 JavaScript 世界中通过实现正确表示:如果as是可选的,它将默认为span

问题是,当as没有通过时,TypeScript 是如何处理这种情况的?我们是否同样传递了默认类型?

好吧,答案是否定的,但下面是一个实际的例子。假设您继续使用Text组件,如下所示:

<Text>Hello Text world</Text>

进入全屏模式 退出全屏模式

请注意,我们在这里没有传递as道具。 TypeScript 会知道这个组件的有效道具吗?

让我们继续添加一个href:

<Text href="https://www.google.com">Hello Text world</Text>

进入全屏模式 退出全屏模式

如果您继续执行此操作,您将不会收到任何错误。那很糟。

span不应接收href道具/属性。虽然我们在实现中默认为span,但 TypeScript 不知道这个默认值。让我们用一个简单的通用默认赋值来解决这个问题:

type TextProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;

/**
* See default below. TS will treat the rendered element as a 
span and provide typings accordingly
*/
export const Text = <C extends React.ElementType = "span">({
  as,
  children,
  ...restProps
}: TextProps<C>) => {
  const Component = as || "span";
  return <Component {...restProps}>{children}</Component>;
};

进入全屏模式 退出全屏模式

重要的一点在下面突出显示:

<C extends React.ElementType = "span">

进入全屏模式 退出全屏模式

瞧!当您将href传递给没有as属性的Text组件时,我们之前的示例现在应该抛出错误。

错误应为:Property 'href' does not exist on type ....[类型上不存在属性 Href](https://res.cloudinary.com/practicaldev/image/fetch/s--hP6PzeaQ--/c_limit%2Cf_auto%2Cfl_progressive% 2Cq_auto%2Cw_880/https://blog.logrocket.com/wp-content/uploads/2022/05/property-href-doesnt-exist-error-1.png)

使用其道具使组件可重用

我们目前的解决方案比我们开始的解决方案要好得多。拍拍自己的后背,让自己走到这一步——它只会从这里变得更有趣。

本节中要迎合的用例在现实世界中非常适用。如果您正在构建某种组件,那么该组件很有可能也会采用该组件独有的一些特定道具。

我们当前的解决方案考虑了aschildren和其他基于as属性的组件属性。但是,如果我们想让这个组件处理它自己的 props 怎么办?

让我们把它变得实用。我们将让Text组件接收color道具。此处的color将是任何彩虹色或black

我们将继续并表示如下:

type Rainbow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "blue"
  | "indigo"
  | "violet";

进入全屏模式 退出全屏模式

接下来,我们必须在TextProps对象中定义color属性,如下所示:

type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black"; // 👈 look here
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;

进入全屏模式 退出全屏模式

在继续之前,让我们进行一些重构。让我们用一个Props对象来表示Text组件的实际 props,并且在TextProps对象中只键入特定于我们组件的 props。

这将变得显而易见,如下所示:

// new "Props" type
type Props <C extends React.ElementType> = TextProps<C>

export const Text = <C extends React.ElementType = "span">({
  as,
  children,
  ...restProps,
}: Props<C>) => {
  const Component = as || "span";
  return <Component {...restProps}>{children}</Component>;
};

进入全屏模式 退出全屏模式

现在让我们清理TextProps:

// before 
type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black"; // 👈 look here
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;

// after
type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black";
};

进入全屏模式 退出全屏模式

现在,TextProps应该只包含特定于我们的Text组件的道具:ascolor

我们现在必须更新Props的定义,以包括我们从TextProps中删除的类型,即childrenReact.ComponentPropsWithoutRef<C>

对于children道具,我们将利用React.PropsWithChildren道具。[PropsWithChildren 类型](https://res.cloudinary.com/practicaldev/image/fetch/s--8xeVMEqc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.logrocket .com/wp-content/uploads/2022/05/propswithchildren-type.png)

PropsWithChildren很容易推理出来。你将你的组件 props 传递给它,它会为你注入子 props 定义:

type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>>

进入全屏模式 退出全屏模式

注意我们如何使用尖括号;这是传递泛型的语法。本质上,React.PropsWithChildren接受您的组件道具作为通用道具,并使用children道具对其进行扩充。甜的!

对于React.ComponentPropsWithoutRef<C>,我们将继续并在此处利用交叉点类型:

type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> & 
React.ComponentPropsWithoutRef<C>

进入全屏模式 退出全屏模式

这是当前的完整解决方案:

type Rainbow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "blue"
  | "indigo"
  | "violet";

type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black";
};

type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> & 
React.ComponentPropsWithoutRef<C>

export const Text = <C extends React.ElementType = "span">({
  as,
  children,
}: Props<C>) => {
  const Component = as || "span";
  return <Component> {children} </Component>;
};

进入全屏模式 退出全屏模式

我知道这些感觉很多,但是当您仔细观察时,一切都会变得有意义。它真的只是把你迄今为止学到的所有东西放在一起!

完成了这个必要的重构之后,我们现在可以继续我们的解决方案了。我们现在所拥有的确实有效。我们已经明确输入了color属性,您可以按如下方式使用它:

<Text color="violet">Hello world</Text>

进入全屏模式 退出全屏模式

严格省略通用组件道具

只有一件事我不太满意:color原来也是许多 HTML 标签的有效属性,就像 HTML5 之前的情况一样。因此,如果我们从类型定义中删除color,它将被接受为任何有效的字符串。

见下文:

type TextProps<C extends React.ElementType> = {
  as?: C;
  // remove color from the definition here
};

进入全屏模式 退出全屏模式

[删除颜色类型定义](https://res.cloudinary.com/practicaldev/image/fetch/s--ivdzWGv4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog .logrocket.com/wp-content/uploads/2022/05/removing-color-type-definition.png)现在,如果您像以前一样继续使用Text,它同样有效:

<Text color="violet">Hello world</Text>

进入全屏模式 退出全屏模式

这里唯一的区别是它是如何输入的。color现在由以下定义表示:

color?: string | undefined

进入全屏模式 退出全屏模式

[默认颜色类型](https://res.cloudinary.com/practicaldev/image/fetch/s--_gpLb590--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog. logrocket.com/wp-content/uploads/2022/05/default-color-type.png)

同样,这不是我们在类型中编写的定义!

这是默认的 HTML 类型,其中color是大多数 HTML 元素的有效属性。有关更多上下文,请参阅这个 Stack Overflow 问题。

两种可能的解决方案

现在,有两种方法可以到达这里。第一个是保留我们最初的解决方案,我们明确声明了color属性:

type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black"; // 👈 look here
};

进入全屏模式 退出全屏模式

第二个选项可以说提供了更多的类型安全性。要实现这一点,您必须了解之前的默认color定义来自何处:React.ComponentPropsWithoutRef<C>。这是根据as的类型添加其他道具的原因。

因此,有了这些信息,我们可以从React.ComponentPropsWithoutRef<C>中显式删除存在于我们的组件类型中的任何定义。

在你看到它之前很难理解它,所以让我们一步一步来。

如前所述,React.ComponentPropsWithoutRef<C>包含基于as类型的所有其他有效属性,例如hrefcolor等,其中这些类型都有自己的定义,例如color?: string | undefined等:[ComponentPropsWithoutRef 类型](https://res.cloudinary.com/practicaldev/image/fetch/s--VjRyJsAF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.logrocket.com/wp-content /uploads/2022/05/componentpropswithoutref-type.png)

可能存在于React.ComponentPropsWithoutRef<C>中的一些值也存在于我们的组件 props 类型定义中。在我们的例子中,两者都存在color![ComponentPropsWithoutRef 和 TextProps](https://res.cloudinary.com/practicaldev/image/fetch/s--VNxNq3XV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.logrocket .com/wp-content/uploads/2022/05/componentpropswithoutref-and-textprops.png)

我们将显式删除组件类型定义中也存在的任何类型,而不是依赖我们的color定义来覆盖来自React.ComponentPropsWithoutRef<C>的内容。[从 ComponentPropsWithoutRef中删除现有道具](https://res.cloudinary.com/practicaldev/image/fetch/s--p5s45qZg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i .imgur.com/Vd2YT3K.png)

因此,如果我们的组件类型定义中存在任何类型,我们将从React.ComponentPropsWithoutRef<C>中显式删除这些类型。

React.ComponentPropsWithoutRef<C>中删除类型

这是我们之前的:

type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> & 
React.ComponentPropsWithoutRef<C>

进入全屏模式 退出全屏模式

我们将更有选择性,而不是添加来自React.ComponentPropsWithoutRef<C>的所有内容的交集类型。我们将使用OmitkeyofTypeScript 实用程序类型来执行一些 TypeScript 魔术。

看一看:

// before 
type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> & 
React.ComponentPropsWithoutRef<C>

// after
type Props <C extends React.ElementType> = 
React.PropsWithChildren<TextProps<C>> &   
Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;

进入全屏模式 退出全屏模式

这是重要的一点:

Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;

进入全屏模式 退出全屏模式

Omit接受两个泛型。第一个是对象类型,第二个是你想从对象类型中“省略”的类型的联合。

这是我最喜欢的例子。考虑一个Vowel对象类型,如下所示:

type Vowels = {
  a: 'a',
  e: 'e',
  i: 'i',
  o: 'o',
  u: 'u'
}

进入全屏模式 退出全屏模式

这是键和值的对象类型。假设我想从Vowels派生一个名为VowelsInOhans的新类型。

好吧,我知道名称Ohans包含两个元音,oa。而不是手动声明这些:

type VowelsInOhans = {
  a: 'a',
  o: 'o'
}

进入全屏模式 退出全屏模式

我可以继续利用Omit,如下所示:

type VowelsInOhans = Omit<Vowels, 'e' | 'i' | 'u'>

进入全屏模式 退出全屏模式

[VowelsInOhans 类型使用省略](https://res.cloudinary.com/practicaldev/image/fetch/s--2q_9-wcI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https:/ /blog.logrocket.com/wp-content/uploads/2022/05/vowelsinohans.png)

Omit将“省略”对象类型Vowels中的eiu键。

另一方面,TypeScript 的keyof运算符的工作方式与您想象的一样。想想 JavaScript 中的Object.keys:给定object类型,keyof将返回对象键的联合类型。

呸!这是一口。这是一个例子:

type Vowels = {
  a: 'a',
  e: 'e',
  i: 'i',
  o: 'o',
  u: 'u'
}

type Vowel = keyof Vowels 

进入全屏模式 退出全屏模式

现在,Vowel将是Vowels的键的联合类型,即:

type Vowel = 'a' | 'e' | 'i' | 'o' | 'u'

进入全屏模式 退出全屏模式

如果你把这些放在一起,再看看我们的解决方案,它们会很好地结合在一起:

Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;

进入全屏模式 退出全屏模式

keyof TextProps<C>返回我们组件道具的键的联合类型。这又被传递给Omit以从React.ComponentPropsWithoutRef<C>中省略它们。

甜的! 🕺

最后,让我们继续将color属性传递给渲染元素:

export const Text = <C extends React.ElementType = "span">({
  as,
  color, // 👈 look here
  children,
  ...restProps
}: Props<C>) => {
  const Component = as || "span";

  // 👇 compose an inline style object
  const style = color ? { style: { color } } : {};

  // 👇 pass the inline style to the rendered element
  return (
    <Component {...restProps} {...style}>
      {children}
    </Component>
  );
};

进入全屏模式 退出全屏模式

为多态类型创建可重用实用程序

我们终于找到了一个行之有效的解决方案。然而,现在让我们更进一步。吨

我们的解决方案非常适合我们的Text组件。但是,如果您希望有一个可以在您选择的任何组件上重用的解决方案,这样您就可以为每个用例提供一个可重用的解决方案怎么办?

让我们开始吧。首先,这是当前没有注释的完整解决方案:

type Rainbow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "blue"
  | "indigo"
  | "violet";

type TextProps<C extends React.ElementType> = {
  as?: C;
  color?: Rainbow | "black";
};

type Props<C extends React.ElementType> = React.PropsWithChildren<
  TextProps<C>
> &
  Omit<React.ComponentPropsWithoutRef<C>, keyof TextProps<C>>;

export const Text = <C extends React.ElementType = "span">({
  as,
  color,
  children,
  ...restProps
}: Props<C>) => {
  const Component = as || "span";

  const style = color ? { style: { color } } : {};

  return (
    <Component {...restProps} {...style}>
      {children}
    </Component>
  );
};

进入全屏模式 退出全屏模式

简洁实用。

如果我们使它可重用,那么它必须适用于任何组件。这意味着删除硬编码的TextProps并用泛型表示 - 因此任何人都可以传入他们需要的任何组件道具。

目前,我们用定义Props<C>表示我们的组件道具。其中C表示为as道具传递的元素类型。

我们现在将其更改为:

// before
Props<C>

// after 
PolymorphicProps<C, TextProps>

进入全屏模式 退出全屏模式

PolymorphicProps代表我们将很快编写的实用程序类型。但是,请注意,它接受两种通用类型,第二种是有问题的组件道具:TextProps

继续定义PolymorphicProps类型:

type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = {} // 👈 empty object for now 

进入全屏模式 退出全屏模式

上面的定义应该是可以理解的。C代表as传入的元素类型,Props是实际的组件props,TextProps

首先,让我们将之前的TextProps拆分为以下内容:

type AsProp<C extends React.ElementType> = {
  as?: C;
};

type TextProps = { color?: Rainbow | "black" };

进入全屏模式 退出全屏模式

因此,我们将AsPropTextProps分开。公平地说,它们代表两种不同的东西。这是一个更好的表示。

现在,让我们更改PolymorphicComponentProp实用程序定义以包括as道具、组件道具和children道具,就像我们过去所做的那样:

type AsProp<C extends React.ElementType> = {
  as?: C;
};

type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>>

进入全屏模式 退出全屏模式

我相信你现在已经明白这里发生了什么:我们有一个交叉类型Props(代表组件道具)和AsProp代表as道具。这些都传入PropsWithChildren以添加children属性定义。出色的!

现在,我们需要包含添加React.ComponentPropsWithoutRef<C>定义的位。但是,我们必须记住忽略组件定义中存在的道具。让我们提出一个强大的解决方案。

写出一个只包含我们想省略的道具的新类型。即AsProp的键和组件道具也是如此。

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

进入全屏模式 退出全屏模式

还记得keyof实用程序类型吗?

PropsToOmit现在将包含我们要省略的 props 的联合类型,它是由P表示的组件的每个 prop 和由AsProps表示的实际多态 propas

把这一切很好地放在PolymorphicComponentProp定义中:

type AsProp<C extends React.ElementType> = {
  as?: C;
};

// before 
type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>>

// after
type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, 
   PropsToOmit<C, Props>>;

进入全屏模式 退出全屏模式

这里重要的是我们添加了以下定义:

Omit<React.ComponentPropsWithoutRef<C>, 
   PropsToOmit<C, Props>>;

进入全屏模式 退出全屏模式

这基本上从React.componentPropsWithoutRef中省略了正确的类型。你还记得omit是如何工作的吗?

尽管看起来很简单,但您现在有了一个可以在不同项目的多个组件上重用的解决方案!

这是完整的实现:

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;

进入全屏模式 退出全屏模式

现在我们可以继续在我们的Text组件上使用PolymorphicComponentProp,如下所示:

export const Text = <C extends React.ElementType = "span">({
  as,
  color,
  children,
  // look here 👇
}: PolymorphicComponentProp<C, TextProps>) => {
  const Component = as || "span";
  const style = color ? { style: { color } } : {};
  return <Component {...style}>{children}</Component>;
};

进入全屏模式 退出全屏模式

多好!如果您构建另一个组件,您可以继续输入它,如下所示:

PolymorphicComponentProp<C, MyNewComponentProps>

进入全屏模式 退出全屏模式

你听到那个声音了吗?那是胜利的声音——你已经走了这么远!

支持多态组件中的引用

您还记得到目前为止对React.ComponentPropsWithoutRef的所有引用吗? 😅 组件道具...... without refs。好吧,现在是时候加入裁判了!

这是我们解决方案的最后也是最复杂的部分。我需要你在这里耐心等待,但我也会尽力详细解释每一步。

首先,你还记得refs在 React 中是如何工作的吗?这里最重要的概念是,您只是不要将ref作为道具传递并期望它像其他所有道具一样传递到您的组件中。在功能组件中处理refs的推荐方法是使用forwardRef函数。

让我们从一个实用的笔记开始。如果您现在继续将ref传递给我们的Text组件,您将收到一个错误,显示为Property 'ref' does not exist on type ...

// Create the ref object 
const divRef = useRef<HTMLDivElement | null>(null);
... 
// Pass the ref to the rendered Text component
<Text as="div" ref={divRef}>
  Hello Text world
</Text>

进入全屏模式 退出全屏模式

[属性引用不存在错误](https://res.cloudinary.com/practicaldev/image/fetch/s--w6ShL9SP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https:// blog.logrocket.com/wp-content/uploads/2022/05/property-ref-doesnt-exist-error.jpeg)

这是意料之中的。

我们第一次支持 refs 将是在Text组件中使用forwardRef,如下所示:

// before 
export const Text = <C extends React.ElementType = "span">({
  as,
  color,
  children,
}: PolymorphicComponentProp<C, TextProps>) => {
  ...
};

// after
import React from "react";

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">({
    as,
    color,
    children,
  }: PolymorphicComponentProp<C, TextProps>) => {
    ...
  }
);

进入全屏模式 退出全屏模式

这实际上只是将之前的代码包装在React.forwardRef中,仅此而已。

现在,React.forwardRef具有以下签名:

React.forwardRef((props, ref) ... )

进入全屏模式 退出全屏模式

本质上,接收到的第二个参数是ref对象。让我们继续处理这个问题:

type PolymorphicRef<C extends React.ElementType> = unknown;

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: PolymorphicComponentProp<C, TextProps>,
    // 👇 look here
    ref?: PolymorphicRef<C>
  ) => {
    ...
  }
);

进入全屏模式 退出全屏模式

我们在这里所做的是添加了第二个参数ref,并将其类型声明为PolymorphicRef,现在它只是指向unknown

请注意,PolymorphicRef接受通用的C。这与之前的解决方案类似——divref对象与span的对象不同,因此我们需要考虑传递给as属性的元素类型。

请注意PolymorphicRef类型。我们如何根据as属性获取ref对象类型?

让我给你一个线索:React.ComponentPropsWithRef!

请注意,这表示_with_ ref。不是_没有_参考。

本质上,如果这是一组键(实际上就是这样),它将包括所有基于元素类型的相关组件道具,以及 ref 对象。[ComponentPropsWithRef 类型](https://res.cloudinary.com/practicaldev/image/fetch/s--mp3yCLd1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://blog.logrocket .com/wp-content/uploads/2022/05/componentpropswithref-type.png)

所以现在,如果我们知道这个对象类型包含ref键,我们不妨通过执行以下操作来获取该引用类型:

// before 
type PolymorphicRef<C extends React.ElementType> = unknown;

// after 
type PolymorphicRef<C extends React.ElementType> =
  React.ComponentPropsWithRef<C>["ref"];

进入全屏模式 退出全屏模式

本质上,React.ComponentPropsWithRef<C>返回一个对象类型,例如,

{
  ref: SomeRefDefinition, 
  // ... other keys, 
  color: string 
  href: string 
  // ... etc
}

进入全屏模式 退出全屏模式

要只选择ref类型,我们可以这样做:

React.ComponentPropsWithRef<C>["ref"];

进入全屏模式 退出全屏模式

请注意,语法类似于 JavaScript 中的属性访问器语法,即["ref"]。现在我们已经输入了ref属性,我们可以继续将其传递给渲染的元素:

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: PolymorphicComponentProp<C, TextProps>,
    ref?: PolymorphicRef<C>
  ) => {
    //...

    return (
      <Component {...style} ref={ref}> // 👈 look here
        {children}
      </Component>
    );
  }
);

进入全屏模式 退出全屏模式

我们取得了不错的进展!事实上,如果你像我们之前那样继续检查Text的使用情况,就不会再出现错误了:

// create the ref object 
const divRef = useRef<HTMLDivElement | null>(null);
... 
// pass ref to the rendered Text component
<Text as="div" ref={divRef}>
  Hello Text world
</Text>

进入全屏模式 退出全屏模式

然而,我们的解决方案仍然没有我想要的那么强类型。让我们继续更改传递给Text的 ref,如下所示:

// create a "button" ref object 
const buttonRef = useRef<HTMLButtonElement | null>(null);
... 
// pass a button ref to a "div". NB: as = "div"
<Text as="div" ref={buttonRef}>
  Hello Text world
</Text>

进入全屏模式 退出全屏模式

TypeScript 应该在这里抛出错误,但它不会。我们正在创建一个button引用,但将其传递给div元素。那是不对的。[通过错误的元素引用时不会引发错误](https://res.cloudinary.com/practicaldev/image/fetch/s--ohnCSWU9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/ https://blog.logrocket.com/wp-content/uploads/2022/05/no-error-throw.png)

如果你看一下ref的确切类型,它看起来像这样:

React.RefAttributes<unknown>.ref?: React.Ref<unknown>

进入全屏模式 退出全屏模式

你看到里面的unknown了吗?这是弱类型的标志。理想情况下,我们应该在其中有HTMLDivElement以将 ref 对象明确定义为div元素 ref。

我们有工作要做。我们先看一下Text组件的其他 props 的类型,它仍然引用PolymorphicComponentProp类型。将其更改为名为PolymorphicComponentPropWithRef的新类型。这将只是PolymorphicComponentProp和 ref 属性的联合。 (你猜对了。)

这里是:

type PolymorphicComponentPropWithRef<
  C extends React.ElementType,
  Props = {}
> = PolymorphicComponentProp<C, Props> & 
{ ref?: PolymorphicRef<C> };

进入全屏模式 退出全屏模式

这只是之前的PolymorphicComponentProp{ ref?: PolymorphicRef<C> }的并集。

现在我们需要更改组件的 props 以引用新的PolymorphicComponentPropWithRef类型:

// before
type TextProps = { color?: Rainbow | "black" };

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: PolymorphicComponentProp<C, TextProps>,
    ref?: PolymorphicRef<C>
  ) => {
    ...
  }
);

// now 
type TextProps<C extends React.ElementType> = 
PolymorphicComponentPropWithRef<
  C,
  { color?: Rainbow | "black" }
>;

export const Text = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: TextProps<C>, // 👈 look here
    ref?: PolymorphicRef<C>
  ) => {
    ...
  }
);

进入全屏模式 退出全屏模式

我们已将TextProps更新为引用PolymorphicComponentPropWithRef,现在它作为Text组件的道具传递。迷人的!

还有最后一件事要做:为Text组件提供类型注释。它看起来类似于:

export const Text : TextComponent = ...

进入全屏模式 退出全屏模式

TextComponent是我们要写的类型注解。这里完整地写出来:

type TextComponent = <C extends React.ElementType = "span">(
  props: TextProps<C>
) => React.ReactElement | null;

进入全屏模式 退出全屏模式

这本质上是一个函数组件,它接收TextProps并返回React.ReactElement | null,其中TextProps如前所述:

type TextProps<C extends React.ElementType> = 
PolymorphicComponentPropWithRef<
  C,
  { color?: Rainbow | "black" }
>;

进入全屏模式 退出全屏模式

有了这个,我们现在有了一个完整的解决方案!

我现在要分享完整的解决方案。一开始可能会让人望而生畏,但请记住,我们已经逐行浏览了您在此处看到的所有内容。带着这种信心阅读它。

import React from "react";

type Rainbow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "blue"
  | "indigo"
  | "violet";

type AsProp<C extends React.ElementType> = {
  as?: C;
};

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

// This is the first reusable type utility we built
type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;

// This is a new type utitlity with ref!
type PolymorphicComponentPropWithRef<
  C extends React.ElementType,
  Props = {}
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };

// This is the type for the "ref" only
type PolymorphicRef<C extends React.ElementType> =
  React.ComponentPropsWithRef<C>["ref"];

/**
* This is the updated component props using PolymorphicComponentPropWithRef
*/
type TextProps<C extends React.ElementType> = 
PolymorphicComponentPropWithRef<
  C,
  { color?: Rainbow | "black" }
>;

/**
* This is the type used in the type annotation for the component
*/
type TextComponent = <C extends React.ElementType = "span">(
  props: TextProps<C>
) => React.ReactElement | null;

export const Text: TextComponent = React.forwardRef(
  <C extends React.ElementType = "span">(
    { as, color, children }: TextProps<C>,
    ref?: PolymorphicRef<C>
  ) => {
    const Component = as || "span";

    const style = color ? { style: { color } } : {};

    return (
      <Component {...style} ref={ref}>
        {children}
      </Component>
    );
  }
);

进入全屏模式 退出全屏模式

你去吧!

结论和下一步的想法

您已经成功构建了一个强大的解决方案,用于在 React 中使用 TypeScript 处理多态组件。我知道这并不容易,但你做到了。

感谢您的关注。记得在官方GitHub 存储库上加注星标,您可以在其中找到本指南的所有代码。如果您想与我分享您对本教程的想法,或者只是联系,您可以在GitHub、LinkedIn或Twitter上找到/关注我。


全面了解生产 React 应用程序

调试 React 应用程序可能很困难,尤其是当用户遇到难以重现的问题时。如果您对监视和跟踪 Redux 状态、自动显示 JavaScript 错误以及跟踪缓慢的网络请求和组件加载时间感兴趣,试试 LogRocket。

LogRocket 注册

LogRocket就像一个用于 Web 和移动应用程序的 DVR,几乎可以记录您的 React 应用程序上发生的所有事情。无需猜测问题发生的原因,您可以汇总并报告问题发生时应用程序所处的状态。 LogRocket 还监控您的应用程序的性能,并使用客户端 CPU 负载、客户端内存使用情况等指标进行报告。

LogRocket Redux 中间件包为您的用户会话增加了一层额外的可见性。 LogRocket 记录来自 Redux 存储的所有操作和状态。

Logo

React社区为您提供最前沿的新闻资讯和知识内容

更多推荐