reactjs - Typescript: hvordan erklære en type som inkluderer alle typer som utvider en felles type?


TLDR: Finnes det en måte i Typescript å erklære en type som omfatter alle typer som utvider et gitt grensesnitt?

Mitt spesifikke problem

Jeg skriver en tilpasset React-hook som kapsler inn logikk for å avgjøre om et element er muset over eller ikke. Den er grovt basert på denne hooken. Den eksponerer en ref som skal kunne ta imot et hvilket som helst HTMLElement:

const ref = useRef<HTMLElement>(null);

Problemet er at hvis jeg prøver å bruke denne refen på et hvilket som helst spesifikt React-element, får jeg en feilmelding som sier at dette spesifikke elementet ikke helt er HTMLElement. For eksempel, hvis jeg bruker det med HTMLDivElement, får jeg denne feilen: argument of type HTMLElement is not assignable to parameter of type HTMLDivElement.

Her er et enkelt repro-eksempel på problemet ovenfor i Typescript-lekeplassen

Åpenbart, jeg ville ikke ønsket å liste opp typer for alle HTML-elementer i hooken min. Gitt at HTMLDivElement utvider HTMLElement-typen, finnes det en måte å erklære at typen jeg faktisk er ute etter ikke er strengt HTMLElement, men hva som helst som utvider HTMLElement?

React kodeeksempel

kildekode til kroken

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

som gir typefeil hvis den brukes slik:

import useHover from 'path-to-useHover';

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

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

}

Typefeil i eksemplet over vil være:

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

Svar #1

Jeg tror du tar feil om retningen på tildelingen som feiler. Hvis du har et grensesnitt A, så er typen som matcher alle underklasser av A bare kalt A. På denne måten er HTMLElement (dvs. kan tildeles fra) et hvilket som helst HTML-element, f.eks. HTMLDivElement.

Dette betyr at hvis du har en rekke funksjoner, der en av dem godtar HTMLDivElement, en annen godtar HTMLLinkElement osv., så finnes det ingen ekte type som du kan sende til alle. Det ville bety at du forventer å ha et element som både er en div og en link og mer.

Redigert basert på dine endringer av spørsmålet: Hvis koden du har fungerer fint, og ditt eneste problem er at den ikke kompilerer, så gjør bare din useHover generisk, slik:

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

Og så:

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

Noe som dette vil få koden din til å kompilere fint, uten å endre kjøretidsoppførselen. Jeg kan ikke si om kjøretidsoppførselen akkurat nå er som ønsket.

Svar #2

Det fungerer som forventet, siden HTMLDivElement utvider HTMLElement. I din TypeScript-playground blandet du det. Jeg oppdaterte det ved å bytte om x og y i denne playgrounden. Du vil at funksjonen skal utvide HTMLElement og sende y, som er en HTMLDivElement, inn i den. Og det fungerer.

Relaterte artikler