Search code examples
javascriptreactjstypescriptstyled-componentsreact-forwardref

How to type forwardRef in separate select component?


The technologies I'm using are react, typescript, and styled-components. I'm trying to create the select component in order to make use of it in React Hook Form. At first, it looks like everything is correct, but I'm getting an error from typescript saying:

No overload matches this call. Overload 1 of 2, '(props: Pick<Pick<Pick<DetailedHTMLProps<SelectHTMLAttributes, HTMLSelectElement>, "form" | ... 262 more ... | "onTransitionEndCapture"> & { ...; } & ISelectProps, "form" | ... 264 more ... | "options"> & Partial<...>, "form" | ... 264 more ... | "options"> & { ...; } & { ...; }): ReactElement<...>', gave the following error. Type 'ForwardedRef' is not assignable to type 'RefObject | (RefObject & ((instance: HTMLSelectElement | null) => void)) | (((instance: HTMLSelectElement | null) => void) & RefObject<...>) | (((instance: HTMLSelectElement | null) => void) & ((instance: HTMLSelectElement | null) => void)) | null | undefined'. Type 'MutableRefObject' is not assignable to type 'RefObject | (RefObject & ((instance: HTMLSelectElement | null) => void)) | (((instance: HTMLSelectElement | null) => void) & RefObject<...>) | (((instance: HTMLSelectElement | null) => void) & ((instance: HTMLSelectElement | null) => void)) | null | undefined'. Type 'MutableRefObject' is not assignable to type '((instance: HTMLSelectElement | null) => void) & RefObject'. Type 'MutableRefObject' is not assignable to type '(instance: HTMLSelectElement | null) => void'. Type 'MutableRefObject' provides no match for the signature '(instance: HTMLSelectElement | null): void'.

Here's my code:

import React, { forwardRef } from "react";
import styled from "styled-components";
import arrow from "../assets/svg/select_arrow.svg";

interface ISelectProps {
  options?: { name: string; value: string | number }[];
  name: string;
  ref?:
    | ((instance: HTMLSelectElement | null) => void)
    | React.RefObject<HTMLSelectElement>
    | null
    | undefined;
}

const SelectContainer = styled.div`
  display: flex;
  align-items: center;
  width: 100%;
  position: relative;
`;

const StyledSelect = styled.select<ISelectProps>`
  width: 100%;
  height: 30px;
  padding: 0 10px;
  background: ${({ theme }) => theme.colors.transparentButton};
  color: white;
  border: 1px solid white;
  border-radius: ${({ theme }) => theme.borderRadius};
  font-size: 14px;
  -moz-appearance: none; /* Firefox */
  -webkit-appearance: none; /* Safari and Chrome */
  appearance: none;
  &:focus,
  &:active {
    outline: none;
  }
`;

const StyledArrow = styled.img`
  position: absolute;
  right: 10px;
  padding: inherit;
`;

const Select = forwardRef(({ options, name }: ISelectProps, ref) => {
  return (
    <>
      <SelectContainer>
        <StyledSelect name={name} ref={ref}>
          {options?.map((op) => (
            <option value={op.value}>{op.name}</option>
          ))}
        </StyledSelect>
        <StyledArrow src={arrow} />
      </SelectContainer>
    </>
  );
});

export default Select;

codesandbox.io/s/eager-hertz-opbsi?file=/src/select.tsx

What Am I doing wrong? Thanks in advance for all the helpful answers!


Solution

  • First of all, ref is not a part of props in this case. You should provide two generic parameters for forwardRef to help TS narrow the types.

    What is ref? It is just HTML element

    import React, { forwardRef } from "react";
    import styled from "styled-components";
    
    interface ISelectProps {
      options?: { name: string; value: string | number }[];
      name: string;
    
    }
    
    const SelectContainer = styled.div`
      display: flex;
      align-items: center;
      width: 100%;
      position: relative;
    `;
    
    const StyledSelect = styled.select<ISelectProps>`
      width: 100%;
      height: 30px;
      padding: 0 10px;
      background: ${({ theme }) => theme.colors.transparentButton};
      color: white;
      border: 1px solid white;
      border-radius: ${({ theme }) => theme.borderRadius};
      font-size: 14px;
      -moz-appearance: none; /* Firefox */
      -webkit-appearance: none; /* Safari and Chrome */
      appearance: none;
      &:focus,
      &:active {
        outline: none;
      }
    `;
    
    const StyledArrow = styled.img`
      position: absolute;
      right: 10px;
      padding: inherit;
    `;
    
    const Select = forwardRef<HTMLSelectElement, ISelectProps>(({ options, name }, ref) => {
      return (
        <>
          <SelectContainer>
            <StyledSelect name={name} ref={ref}>
              {options?.map((op) => (
                <option value={op.value}>{op.name}</option>
              ))}
            </StyledSelect>
          </SelectContainer>
        </>
      );
    });
    
    export default Select;
    

    Playground link