skip to Main Content

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: "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
  });
});

The error states:

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

2

Answers


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

    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, 
        });
      }
    });
    
    Login or Signup to reply.
  2. 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 "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>;
    
    

    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 = <T extends Tag>({ as, ...props }: Props<T>) =>
      createElement(as, props);
    

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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search