如何在传递道具时使用 TypeScript 实现“as”道具?

Posted

技术标签:

【中文标题】如何在传递道具时使用 TypeScript 实现“as”道具?【英文标题】:How can I implement a "as" prop with TypeScript while passing down the props? 【发布时间】:2021-05-08 23:40:20 【问题描述】:

我正在构建一个组件库,我需要其中一些组件具有可自定义的标签名称。例如,有时看起来像 <button> 的东西实际上是 <a>。所以我希望能够像这样使用按钮组件:

<Button onClick=onClick>Click me!</Button>
<Button as="a" href="/some-url">Click me!</Button>

理想情况下,我希望根据“as”道具推断可用道具:

// Throws an error because the default value of "as" is "button",
// which doesn't accept the "href" attribute.
<Button href="/some-url">Click me!<Button>

我们可能还需要传递一个自定义组件:

// Doesn't throw an error because RouterLink has a "to" prop
<Button as=RouterLink to="/">Click me!</Button>

这是没有 TypeScript 的实现:

function Button( as = "button", children, ...props ) 
  return React.createElement(as, props, children);

那么,如何在传递道具的同时使用 TypeScript 实现“as”道具?

注意:我基本上是在尝试做styled-components 所做的事情。但是我们正在使用 CSS 模块和 SCSS,所以我负担不起添加样式组件。不过,我愿意接受更简单的替代方案。

【问题讨论】:

这就像那里的 90%,但由于某种原因,T extends ComponentType&lt;any&gt; 的重载匹配了它不应该匹配的东西:tsplay.dev/rw2j1w 您也许可以从 styled-components 包中复制一些类型声明. 谢谢,这是一个非常有趣的实现????我们将有许多与此类似的组件,因此希望我能找到一种使类型可重用的方法。我将研究样式化组件和其他库的定义。在这一点上,我也可以采用更天真的方法。 【参考方案1】:

新答案

我最近看到了 Iskander Samatov 的文章 React polymorphic components with TypeScript,他们在其中分享了一个更完整、更简单的解决方案:

import * as React from "react";

interface ButtonProps<T extends React.ElementType> 
  as?: T;
  children?: React.ReactNode;


function Button<T extends React.ElementType = "button">(
  as,
  ...props
:
  ButtonProps<T>
  & Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonProps<T>>
) 
  const Component = as || "button";
  return <Component ...props />;

Typescript playground

旧答案

我花了一些时间研究 styled-components 的类型声明。我能够提取最低要求的代码,这里是:

import * as React from "react";
import  Link  from "react-router-dom";

type CustomComponentProps<
  C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
  O extends object
> = React.ComponentPropsWithRef<
  C extends keyof JSX.IntrinsicElements | React.ComponentType<any> ? C : never
> &
  O &  as?: C ;

interface CustomComponent<
  C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
  O extends object
> 
  <AsC extends keyof JSX.IntrinsicElements | React.ComponentType<any> = C>(
    props: CustomComponentProps<AsC, O>
  ): React.ReactElement<CustomComponentProps<AsC, O>>;


const Button: CustomComponent<"button",  variant: "primary" > = (props) => (
  <button ...props />
);

<Button variant="primary">Test</Button>;
<Button variant="primary" to="/test">
  Test
</Button>;
<Button variant="primary" as=Link to="/test">
  Test
</Button>;
<Button variant="primary" as=Link>
  Test
</Button>;

TypeScript playground

我从 styled-components 中删除了很多比这更复杂的东西。例如,他们有一些解决方法来处理我删除的类组件。所以这个 sn-p 可能需要针对高级用例进行定制。

【讨论】:

【参考方案2】:

我发现你可以用 JSX.IntrinsicElements 做同样的事情。我有面板元素:

export type PanelAsKeys = 'div' | 'label'
export type PanelAsKey = Extract<keyof JSX.IntrinsicElements, PanelAsKeys>
export type PanelAs<T extends PanelAsKey> = JSX.IntrinsicElements[T]
export type PanelAsProps<T extends PanelAsKey> = Omit<PanelAs<T>, 'className' | 'ref'>

我省略了像 ref 和 className 这样的原生类型,因为我对这些字段有自己的类型

这就是道具的样子

export type PanelProps<T extends PanelAsKey = 'div'> = PanelAsProps<T> & 

const Panel = <T extends PanelAsKey = 'div'>(props: PanelProps<T>) => 

然后你可以使用 React.createElement

return React.createElement(
  as || 'div',
  
    tabIndex: tabIndex || -1,
    onClick: handleClick,
    onFocus: handleFocus,
    onBlur: handleBlur,
    onMouseEnter: handleMouseEnter,
    onMouseLeave: handleMouseLeave,
    'data-testid': 'panel',
    ...rest
  ,
  renderChildren)

在这种情况下,我没有看到 ts 错误,并且对像 htmlFor 这样的标签道具有补全

【讨论】:

以上是关于如何在传递道具时使用 TypeScript 实现“as”道具?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Material UI 和 TypeScript 将自定义道具传递给样式化组件?

如何将道具从调用组件传递给 HOC - React + Typescript

样式化的 MUI 无法在 typescript 中将组件道具传递给 Typography

反应导航:如何在打字稿中使用 NavigationStackScreenComponent 类型传递 redux 道具

如何使用 Typescript 在不丢失任何道具的情况下键入样式组件?

使用 graphql 将道具传递给高阶组件时出现 React-apollo Typescript 错误