downshift icon indicating copy to clipboard operation
downshift copied to clipboard

TypeScript compiler error with ref passed from getInputProps to component using a forwardRef

Open fenech opened this issue 6 years ago • 9 comments

  • downshift version: 3.2.10
  • node version: v11.15.0
  • npm (or yarn) version: 6.7.0
  • react version: 16.8.4
  • styled-components version: 4.3.1

Relevant code or config

import * as React from "react";
import { render } from "react-dom";
import styled from "styled-components";
import Downshift from "downshift";

const Input = styled.input`
  width: 200px;
`;

function App() {
  return (
    <Downshift>
      {({ getInputProps }) => (
        <div>
          <Input
            {...getInputProps({
              onKeyUp(e: React.KeyboardEvent<HTMLInputElement>) {
                // handle key up
              }
            })}
          />
        </div>
      )}
    </Downshift>
  );
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);

What you did: Attempted to pass a custom getInputProps to a component which implements a forward ref (in this case, to an <input> element).

What happened: A compiler error from TypeScript:

Type '{ onKeyUp: ((e: KeyboardEvent<HTMLInputElement>) => void) & ((event: KeyboardEvent<HTMLInputElement>) => void); disabled?: boolean; accept?: string; acceptCharset?: string; action?: string; ... 354 more ...; key?: Key; }' is not assignable to type 'Pick<Pick<Pick<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "form" | "style" | "title" | "pattern" | "onChange" | "onSelect" | "children" | ... 277 more ... | "onTransitionEndCapture"> & { ...; }, "form" | ... 284 more ... | "onTransitionEndCapture"> & Partial<...>, "form" | ... 284 mo...'.
  Types of property 'ref' are incompatible.
    Type 'LegacyRef<HTMLInputElement>' is not assignable to type '((instance: HTMLInputElement) => void) | RefObject<HTMLInputElement>'.
      Type 'string' is not assignable to type '((instance: HTMLInputElement) => void) | RefObject<HTMLInputElement>'.ts(2322)

Reproduction repository: https://codesandbox.io/s/loving-diffie-njech

Problem description: Type of ref is incompatible, even though the component appears to behave correctly with the forwarded ref.

Suggested solution: Change the type definitions to (optionally?) remove the LegacyRef part.

fenech avatar Jun 18 '19 17:06 fenech

I looked into the codesandbox but I don't see any error with ref. Instead I see one with the <Input> you are using, that it does no accept onKeyUp (or onKeyDown for instance). If you cast it to any const Input: any then it works for me.

Try to fix it at your end or change the codesandbox with the relevant error. Thank you!

silviuaavram avatar Jun 20 '19 07:06 silviuaavram

@silviuavram the error is there, it says Types of property 'ref' are incompatible. (the second line of the error message in my original post). I included one custom prop onKeyUp to make the use case more realistic but you can remove it and pass an empty object {} to see the same error, slightly less hidden.

Typing the return value of the function as any is indeed what I have done in my application, but I don't think that disabling type checking is the best solution!

fenech avatar Jun 20 '19 07:06 fenech

I'm also getting this error and the 'error with ref' message is clear in the codesandbox repro that @fenech created.

stevejay avatar Jul 20 '19 12:07 stevejay

I ran into this as well, it seems to be caused by components that are forwarding the ref. I'm assuming styled does this. I was able to replicate it just creating a super simple component that wraps a std <input> and uses React.forwardRef vs just using the base input element does not cause any errors.

Looking at the react types for 16.9 there are two ref types:

 type Ref<T> = { bivarianceHack(instance: T | null): void }["bivarianceHack"] | RefObject<T> | null;
type LegacyRef<T> = string | Ref<T>;

and the typing for the InputProps loads the React.HTMLProps which uses LegacyRef

export interface GetInputPropsOptions
  extends React.HTMLProps<HTMLInputElement> {
  disabled?: boolean
}

It looks like what happening is that the typing for React.forwardRef does not support the LegacyRef (aka string), it's just using the regular ref:

    interface RefForwardingComponent<T, P = {}> {
        (props: PropsWithChildren<P>, ref: Ref<T>): ReactElement | null;
        propTypes?: WeakValidationMap<P>;
        contextTypes?: ValidationMap<any>;
        defaultProps?: Partial<P>;
        displayName?: string;
    }

All that being said, I'm not sure how to fix it.

Edit, overriding the types as a temporary fix works:

interface GetInputPropsOptionsRef extends GetInputPropsOptions {
  ref?: Ref<HTMLInputElement>
}
<Input {...getInputProps({}) as GetInputPropsOptionsRef}  />

kirkchris avatar Oct 11 '19 04:10 kirkchris

Can someone create a PR if there is a fix? Please also look at the useSelect and useCombobox typings maybe they need to be fixed as well. Thanks!

silviuaavram avatar Dec 20 '19 13:12 silviuaavram

Similarly, to get around this issue I've been creating new types that look like this:

export type IntrinsicDiv = JSX.IntrinsicElements['div'] & { ref?: Ref<HTMLDivElement> };

ironspecs avatar May 30 '20 18:05 ironspecs

I fix up the ref type like this, but it feels wrong:

type ConvertRef<T, Target extends HTMLElement> = Omit<T, 'ref'> & { ref?: Ref<Target> }

fabb avatar Jul 30 '20 07:07 fabb

getInputProps doesn't even return a ref unless given one so should it even give an opinion on what that ref should look like?

MarkFalconbridge avatar Feb 05 '21 12:02 MarkFalconbridge

I'm trying the following:


 <Downshift>
      {({ getInputProps }) => (
        <div>
          <TextField
          variant="filled"
            {...getInputProps({
           
            })}
          />
        </div>
      ) as GetInputPropsOptionsRef} 
    </Downshift>

after declaring:

interface GetInputPropsOptionsRef extends GetInputPropsOptions {
  ref?: Ref<HTMLInputElement>
}

but I still get the same error:

Type '{ disabled?: boolean | undefined; accept?: string | undefined; acceptCharset?: string | undefined; action?: string | undefined; allowFullScreen?: boolean | undefined; allowTransparency?: boolean | undefined; ... 357 more ...; variant: "outlined"; }' is not assignable to type 'IntrinsicAttributes & TextFieldProps'. Type '{ disabled?: boolean | undefined; accept?: string | undefined; acceptCharset?: string | undefined; action?: string | undefined; allowFullScreen?: boolean | undefined; allowTransparency?: boolean | undefined; ... 357 more ...; variant: "outlined"; }' is not assignable to type 'OutlinedTextFieldProps'. Types of property 'onChange' are incompatible. Type 'FormEventHandler<HTMLInputElement> | undefined' is not assignable to type 'ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> | undefined'. Type 'FormEventHandler<HTMLInputElement>' is not assignable to type 'ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>'. Types of parameters 'event' and 'event' are incompatible. Type 'ChangeEvent<HTMLInputElement | HTMLTextAreaElement>' is not assignable to type 'FormEvent<HTMLInputElement>'. Types of property 'currentTarget' are incompatible. Type 'EventTarget & (HTMLInputElement | HTMLTextAreaElement)' is not assignable to type 'EventTarget & HTMLInputElement'. Type 'EventTarget & HTMLTextAreaElement' is not assignable to type 'EventTarget & HTMLInputElement'. Type 'EventTarget & HTMLTextAreaElement' is missing the following properties from type 'HTMLInputElement': accept, align, alt, capture, and 26 more.ts(2322) (alias) function TextField(props: TextFieldProps): JSX.Element

Does someone have any idea how to fix it? I'm using React 17.0.2 and downshift 6.1.7 and material-ui 5.0.0

alvaradojl avatar Oct 25 '21 12:10 alvaradojl