如何在创建条件型的 Anchor 或 Button 组件时使用 React.forwardRef?

huangapple go评论66阅读模式
英文:

How to type React.forwardRef when creating conditional Anchor or Button component?

问题

以下是您要翻译的代码部分:

type ConditionalProps =
  | ({
      as: "button";
    } & React.ButtonHTMLAttributes<HTMLButtonElement>)
  | ({
      as: "a";
    } & React.AnchorHTMLAttributes<HTMLAnchorElement>);

const Trigger = forwardRef<
  HTMLAnchorElement | HTMLButtonElement, // <- this is an issue
  ConditionalProps 
>(function Trigger({ as, ...props }, ref) {
  return createElement(as, {
    ...props,
    ref, // <- error
  });
});

错误信息如下:

Type 'ForwardedRef<HTMLAnchorElement | HTMLButtonElement>' is not
assignable to type 'Ref<HTMLAnchorElement> | undefined'.
英文:

I'm creating a component that can be a Button or Anchor tag. But I'm having trouble making a conditional typing for the ref. How can I get the ref to be accepted?

type ConditionalProps =
  | ({
      as: &quot;button&quot;;
    } &amp; React.ButtonHTMLAttributes&lt;HTMLButtonElement&gt;)
  | ({
      as: &quot;a&quot;;
    } &amp; React.AnchorHTMLAttributes&lt;HTMLAnchorElement&gt;);

const Trigger = forwardRef&lt;
  HTMLAnchorElement | HTMLButtonElement, // &lt;- this is an issue
  ConditionalProps 
&gt;(function Trigger({ as, ...props }, ref) {
  return createElement(as, {
    ...props,
    ref, // &lt;- error
  });
});

The error states:

Type &#39;ForwardedRef&lt;HTMLAnchorElement | HTMLButtonElement&gt;&#39; is not
assignable to type &#39;Ref&lt;HTMLAnchorElement&gt; | undefined&#39;.

答案1

得分: 1

你可以根据获得的 as 属性有两个不同的 return 语句,并对给定的类型使用 TypeScript 断言。

type ConditionalProps =
  | ({
      as: 'button';
    } & React.ButtonHTMLAttributes<HTMLButtonElement>)
  | ({
      as: 'a';
    } & React.AnchorHTMLAttributes<HTMLAnchorElement>);

const Trigger = forwardRef<
  HTMLAnchorElement | HTMLButtonElement,
  ConditionalProps
>((function Trigger({ as, ...props }, ref) {
  const elementType = 'button';

  if (as === 'a') {
    const aRef = ref as Ref<HTMLAnchorElement> | undefined;
    return createElement(as, {
      ...props,
      aRef,
    });
  } else {
    const buttonRef = ref as Ref<HTMLButtonElement> | undefined;
    return createElement(as, {
      ...props,
      buttonRef,
    });
  }
});
英文:

You could have two return statements depending on the as property you get, and use TypeScript assertions for the given type.

type ConditionalProps =
  | ({
      as: &#39;button&#39;;
    } &amp; React.ButtonHTMLAttributes&lt;HTMLButtonElement&gt;)
  | ({
      as: &#39;a&#39;;
    } &amp; React.AnchorHTMLAttributes&lt;HTMLAnchorElement&gt;);

const Trigger = forwardRef&lt;
  HTMLAnchorElement | HTMLButtonElement, 
  ConditionalProps
&gt;(function Trigger({ as, ...props }, ref) {
  const elementType = &#39;button&#39;;

  if (as === &#39;a&#39;) {
    const aRef = ref as Ref&lt;HTMLAnchorElement&gt; | undefined;
    return createElement(as, {
      ...props,
      aRef, 
    });
  } else {
    const buttonRef = ref as Ref&lt;HTMLButtonElement&gt; | undefined;
    return createElement(as, {
      ...props,
      buttonRef, 
    });
  }
});

答案2

得分: 0

尝试了您的代码后,我没有收到类型错误,联合类型在https://codesandbox.io/s/react-typescript-forked-elwo35?file=/src/App.tsx 上似乎运行正常。

但是对于这种情况,我认为泛型通常效果很好,因为只涉及一种类型,所以相当简单。

所以通常可以设置一些类型,为了清晰起见,这里有点冗长。

import { HTMLAttributes } from "react";

type TagElementMap = {
  readonly button: HTMLButtonElement;
  readonly a: HTMLAnchorElement;
};

type Tag = keyof TagElementMap;
type TagElement<T extends Tag> = TagElementMap[T];
type TagAttributes<T extends Tag> = HTMLAttributes<TagElementMap[T]>;

type Props<T extends Tag> = {
  as: T;
  myRef: RefObject<TagElement<T>>;
} & TagAttributes<T>;

然后,您可以使您的组件具有泛型。在此示例中,我使用了一个自定义的 ref 属性,而不是 forwardRef。逻辑是相同的,只是使用 forwardRef 麻烦一些,因为您必须在另一个具有自定义 ref 的组件中包装整个内容,然后将其传递给泛型。

const Trigger = <T extends Tag>({ as, ...props }: Props<T>) =>
  createElement(as, props);

这里是一个完整示例 https://codesandbox.io/s/react-typescript-forked-k2flhc?file=/src/App.tsx

英文:

Trying your code I didnt get type errors, union seemed fine on https://codesandbox.io/s/react-typescript-forked-elwo35?file=/src/App.tsx

But whenever for these kind of situations I think generics work well, its just one type so fairly simple.

So usually can set up some types like this, bit verbose here for clarity.

import { HTMLAttributes } from &quot;react&quot;;

type TagElementMap = {
  readonly button: HTMLButtonElement;
  readonly a: HTMLAnchorElement;
};

type Tag = keyof TagElementMap;
type TagElement&lt;T extends Tag&gt; = TagElementMap[T];
type TagAttributes&lt;T extends Tag&gt; = HTMLAttributes&lt;TagElementMap[T]&gt;;

type Props&lt;T extends Tag&gt; = {
  as: T;
  myRef: RefObject&lt;TagElement&lt;T&gt;&gt;;
} &amp; TagAttributes&lt;T&gt;;

Then you can make your component generic, in this example I'm using a custom ref prop instead of forward, the logic is the same it's just annoying to do with forwardref you have to wrap the whole thing in another component with custom ref anyway and then forward that one with generic..

const Trigger = &lt;T extends Tag&gt;({ as, ...props }: Props&lt;T&gt;) =&gt;
  createElement(as, props);

Here's a full example https://codesandbox.io/s/react-typescript-forked-k2flhc?file=/src/App.tsx

huangapple
  • 本文由 发表于 2023年3月12日 10:15:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/75710718.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定