简体   繁体   中英

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'.

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 (ie is assignable from ) any HTML element, eg 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.

It works as expected, since HTMLDivElement extends HTMLElement. In your typescirpt playground you mixed it up. I updated it by switching x and y in this playground . You want the function to extend HTMLElement and pass y, which is and HTMLDivElement into it. And that works.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM