Typescript:如何声明一个包含所有扩展通用类型的类型的类型?

时间:2019-07-25 11:44:48

标签: reactjs typescript

TLDR: Typescript中是否有一种方法可以声明包含扩展给定接口的所有类型的类型?

我的具体问题

我正在编写一个自定义的React挂钩,该挂钩封装了用于确定是否将鼠标悬停在元素上的逻辑。它是在this hook之后大致建模的。它公开了一个应该可以使用任何HTMLElement的引用:

const ref = useRef<HTMLElement>(null);

问题是,如果我尝试在任何特定的React元素上使用此ref,则会收到一条错误消息,告诉我该特定元素不是HTMLElement。例如,如果我将其与HTMLDivElement一起使用,则会出现以下错误:argument of type HTMLElement is not assignable to parameter of type HTMLDivElement

这里是a simple repro case of the problem above in Typescript playground

很显然,我不想在我的钩子中列出所有html元素的类型。假设HTMLDivElement扩展了HTMLElement类型,是否有一种方法可以声明我实际上追求的类型不是严格的HTMLElement,而是任何扩展了HTMLElement的东西?


反应代码示例

钩子的源代码

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];
}

如果这样使用将产生类型错误:

import useHover from 'path-to-useHover';

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

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

}

上面示例中的类型错误将是:

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

2 个答案:

答案 0 :(得分:1)

我认为您对失败的分配方向有误。如果您有接口A,则与A的所有子类匹配的类型称为A。这样,HTMLElement(即可以从分配)任何HTML元素,例如HTMLDivElement

这意味着,如果您有一堆函数,其中一个函数接受HTMLDivElement,另一个函数接受HTMLLinkElement等,则没有可以传递给所有函数的真实类型。这意味着您希望拥有一个既是div又是链接以及更多内容的元素。

根据您对问题的编辑进行编辑: 如果您拥有的代码可以正常工作,并且唯一的问题是它无法编译,则只需将useHover泛型化,就像这样:

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

然后:

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

这样的事情可以使您的代码正常编译,而无需更改其运行时行为。我无法确定当前的运行时行为是否符合期望。

答案 1 :(得分:0)

由于HTMLDivElement扩展了HTMLElement,因此可以正常工作。在您的typecirpt游乐场中,您将其混合了。我通过在此playground中切换x和y来更新它。您希望函数扩展HTMLElement并传递y(即HTMLDivElement)。那行得通。