Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: how to declare a type that includes all types extending a common type?

TLDR: Is there a way in Typescript to declare a type that encompasses all types that extend a given interface?

My specific problem

I am writing a custom React hook that encapsulates logic for deciding whether or not an element is moused over. It is modelled roughly after this hook. It exposes a ref that should be able to take any HTMLElement:

const ref = useRef<HTMLElement>(null);

The problem is, if I try to use this ref on any specific React element, I get an error telling me that this specific element is not quite HTMLElement. For example, if I use it with HTMLDivElement, I get this error: argument of type HTMLElement is not assignable to parameter of type HTMLDivElement.

Here's a simple repro case of the problem above in Typescript playground

Obviously, I wouldn't want to list types of all html elements in my hook. Given that HTMLDivElement extends the HTMLElement type, is there a way of declaring that the type that I am actually after is not strictly HTMLElement, but whatever extends HTMLElement?


React code example

source code of the hook

import { useRef, useState, useEffect } from 'react';

type UseHoverType = [React.RefObject<HTMLElement>, boolean];

export default function useHover(): UseHoverType {
  const [isHovering, setIsHovering] = useState(false);
  let isTouched = false;

  const ref = useRef<HTMLElement>(null); // <-- What should the type be here?

  const handleMouseEnter = () => {
    if (!isTouched) {
      setIsHovering(true);
    }
    isTouched = false;
  };
  const handleMouseLeave = () => {
    setIsHovering(false);
  };

  const handleTouch = () => {
    isTouched = true;
  };

  useEffect(() => {
    const element = ref.current;
    if (element) {
      element.addEventListener('mouseenter', handleMouseEnter);
      element.addEventListener('mouseleave', handleMouseLeave);
      element.addEventListener('touchstart', handleTouch);

      return () => {
        element.removeEventListener('mouseenter', handleMouseEnter);
        element.removeEventListener('mouseleave', handleMouseLeave);
        element.removeEventListener('touchend', handleTouch);
      };
    }
  }, [ref.current]);

  return [ref, isHovering];
}

which produces type error if used like this:

import useHover from 'path-to-useHover';

const testFunction = () => {
  const [hoverRef, isHovered] = useHover();

  return (
     <div
      ref={hoverRef}
     >
      Stuff
     </div>
  );

}

Type error in example above will be:

Type 'RefObject<HTMLElement>' is not assignable to type 'string | RefObject<HTMLDivElement> | ((instance: HTMLDivElement | null) => void) | null | undefined'.
  Type 'RefObject<HTMLElement>' is not assignable to type 'RefObject<HTMLDivElement>'.
    Property 'align' is missing in type 'HTMLElement' but required in type 'HTMLDivElement'.
like image 534
azangru Avatar asked Jul 25 '19 11:07

azangru


Video Answer


1 Answers

I think you are mistaken about the direction of the assignment that fails. If you have an interface A, then the type that matches all subclasses of A is just called A. This way, HTMLElement (i.e. is assignable from) any HTML element, e.g. HTMLDivElement.

This means that if you have a bunch of functions, one of them accepts HTMLDivElement, another accepts HTMLLinkElement etc, then there is no real type that you can pass to all of them. It would mean you expect to have an element that is both a div and a link and more.

Edited based on your edits of the question: If the code you have works fine, and your only problem is that it doesn't compile, then just make your useHover generic, like this:

type UseHoverType<T extends HTMLElement> = [React.RefObject<T>, boolean];

function useHover<T extends HTMLElement>(): UseHoverType<T> {
  const ref = useRef<T>(null); // <-- What should the type be here?
  ...

And then:

const testFunction = () => {
  const [hoverRef, isHovered] = useHover<HTMLDivElement>();

Something like this will make your code compile fine, without changing its runtime behaviour. I'm unable to tell if the runtime behaviour right now is as desired.

like image 170
TPReal Avatar answered Sep 30 '22 13:09

TPReal