javascript - tip ForwardRef bazat pe proprietăți


Folosesc React + TypeScript. Lucrez la un component care poate returna un element HTML dinamic, în funcție de props:

interface Props {
  label?: string;
}

const DynamicComponent = forwardRef<
  HTMLButtonElement | HTMLLabelElement,
  Props
>((props, ref) => {
  if (props.label) {
    return (
      <label ref={ref as React.ForwardedRef<HTMLLabelElement>}>
        {props.label}
      </label>
    );
  }
  return (
    <button ref={ref as React.ForwardedRef<HTMLButtonElement>}>BUTTON</button>
  );
});

Este posibil să tipăriți interfața lui ref într-un mod care să depindă de proprietatea label?

export default function App() {
  const btnRef = useRef<HTMLButtonElement>(null); // Allowed
  const labelRef = useRef<HTMLLabelElement>(null); // Allowed
 //  const labelRef = useRef<HTMLButtonElement>(null); // Not allowed, because `label` prop was not provided
  return (
    <>
      <DynamicComponent ref={btnRef} />
      <DynamicComponent ref={labelRef} label="my label" />
    </>
  );
}

Link către sandbox

Răspuns #1

Pentru a face asta, trebuie să folosim supraincarcarea funcțiilor, cu modelul de funcții de ordin superior și tipuri de protecție:

FIX

import React, { forwardRef, useRef, Ref } from 'react'

interface Props {
  label?: string;
}

// typeguard
const isLabelRef = (props: Props, ref: React.ForwardedRef<any>): ref is React.ForwardedRef<HTMLLabelElement> => {
  return true // ! NON IMPLEMENTED
}

// typeguard
const isButtonRef = (props: Props, ref: React.ForwardedRef<any>): ref is React.ForwardedRef<HTMLButtonElement> => {
  return true // ! NON IMPLEMENTED
}

// Higher order COmponent with overloads
function DynamicComponent<T extends HTMLButtonElement>(reference: Ref<T>): any
function DynamicComponent<T extends HTMLLabelElement>(reference: Ref<T>, props: Props): any
function DynamicComponent<T extends HTMLElement>(reference: Ref<T> | undefined, props?: Props) {

  const WithRef = forwardRef<HTMLElement, Props>((_, ref) => {
    if (props && isLabelRef(props, ref)) {
      return (
        <label ref={ref}>
          {props.label}
        </label>
      );
    }

    if (props && isButtonRef(props, ref)) {
      return (
        <button ref={ref}>BUTTON</button>
      );
    }
    return null
  });

  return <WithRef ref={reference} />
}


export default function App() {
  const btnRef = useRef<HTMLButtonElement>(null); // Allowed
  const labelRef = useRef<HTMLLabelElement>(null); // Allowed

  return (
    <>
      {DynamicComponent(btnRef)}
      {DynamicComponent(labelRef, { label: 'sdf' })}
    </>
  );
}

Teren de joacă

După cum ați observat, eu folosesc doar props din DynamicComponent.

De asemenea, parametrul generic T servește pentru a restrânge tipul ref

Am lăsat isButtonRef și isLabelRef neimplementate

ACTUALIZARE Se pare că exemplul meu anterior este inutil. Îmi pare rău pentru asta.

Scuze. Am rezolvat deja problema.

Ca soluție alternativă, poți suprascrie funcția încorporată forwardRef.

interface Props {
  label: string;
}

declare module "react" {
  function forwardRef<T extends HTMLButtonElement, P>(
    render: ForwardRefRenderFunction<HTMLButtonElement, never>
  ): ForwardRefExoticComponent<
    PropsWithRef<{ some: number }> & RefAttributes<HTMLButtonElement>
  >;

  function forwardRef<T extends HTMLLabelElement, P extends { label: string }>(
    render: ForwardRefRenderFunction<HTMLLabelElement, { label: string }>
  ): ForwardRefExoticComponent<
    PropsWithRef<{ label: string }> & RefAttributes<HTMLLabelElement>
  >;
}

const WithLabelRef = forwardRef<HTMLLabelElement, Props>((props, ref) => (
  <label ref={ref}>{props.label}</label>
));

const WithButtonRef = forwardRef<HTMLButtonElement>((props, ref) => (
  <button ref={ref}>{props}</button>
));

function App() {
  const btnRef = useRef<HTMLButtonElement>(null);
  const labelRef = useRef<HTMLLabelElement>(null);
  const divRef = useRef<HTMLDivElement>(null);

  return (
    <>
      <WithButtonRef ref={btnRef} />
      <WithLabelRef ref={labelRef} label="my label" />
      <WithLabelRef ref={divRef} label="my label" /> //expected error
    </>
  );
}