skip to Main Content

I have a library function that returns a ref which needs to be passed to some element. The library doesn’t care which type of element it is, so I’d like to type it as React.Ref<HTMLElement>.

The problem is, assignment rule for refs is backwards. Ref<HTMLElement> can’t be passed to a div element or any other element for that matter.

If I make the library return Ref<HTMLDivElement> then it can’t be applied to any other element type.

What’s the correct pattern for that, and why is react ref typed that way?

2

Answers


  1. Try to type it like:

    import React, { useEffect, useRef, RefObject } from 'react';
    
    type UseYo<T extends HTMLElement> = {
      ref: RefObject<T>;
    };
    
    function useYo<T extends HTMLElement>(): UseYo<T>{
      const ref = useRef<T>(null);
    
      useEffect(() => {
        if (!ref.current) {
          return;
        }
        // do something with ref
      }, [])
    
      return { ref };
    }
    
    function MyComponent() {
      const { ref } = useYo<HTMLDivElement>();
    
      return (
        <div ref={ref}></div>
      );
    }
    

    See it on the playground

    Login or Signup to reply.
  2. As I understand it, this is a question of covariance and contravariance.

    To oversimplify, covariance means that you can substitute an instance of a more specific type when passing a parameter.

    function doSomething(el: HTMLElement) { /* ... */ }
    
    const myDiv: HTMLDivElement;
    doSomething(myDiv); // ok, myDiv is an HTMLELement
    

    However, function parameters are contravariant:

    function doSomething(callback: (el: HTMLElement) => void) { /* ... */ }
    
    const myCallback: (el: HTMLDivElement) => void;
    doSomething(myCallback); // No good!
    // myCallback may be called with *any* HTMLElement, but it only says it takes divs
    

    Refs are more like the function parameters example. The type system doesn’t "know" that a ref is an output-only parameter; in actuality, it’s more like the following:

    interface Ref<T> { current: T };
    
    function Div(ref: Ref<HTMLDivElement>);
    
    const myRef: Ref<HTMLElement>;
    Div({ref: myRef}); // No good!
    // Div says it only takes divs, but ref.current may be *any* HTMLElement
    

    Unfortunately, I don’t know of a clean solution within TypeScript’s type system. You can resort to typecasts:

    function MyComponent() {
      const myRef = useRefFromMyLibrary();
      return <div ref={myRef as Ref<HTMLDivElement>} />;
    }
    

    Or use generics, as in @moonwave99’s solution.

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