[React Typescript] Strongly typed React component `as`

The `as` Prop in React

Option 1:

import { Equal, Expect } from '../helpers/type-utils';

export const Wrapper = <TProps extends keyof JSX.IntrinsicElements>(
  props: {
    as: TProps;
  } & JSX.IntrinsicElements[TProps]
) => {
  const Comp = props.as as string;
  return <Comp {...(props as JSX.IntrinsicElements[TProps])}></Comp>;
};

const Example1 = () => {
  return (
    <>
      <Wrapper
        as="button"
        // @ts-expect-error doesNotExist is not a valid prop
        doesNotExist
      ></Wrapper>

      <Wrapper
        as="button"
        // e should be inferred correctly
        onClick={(e) => {
          type test = Expect<
            Equal<typeof e, React.MouseEvent<HTMLButtonElement>>
          >;
        }}
      ></Wrapper>
    </>
  );
};

/**
 * Should work specifying a 'div'
 */

const Example2 = () => {
  return (
    <>
      <Wrapper
        as="div"
        // @ts-expect-error doesNotExist is not a valid prop
        doesNotExist
      ></Wrapper>

      <Wrapper
        as="div"
        // e should be inferred correctly
        onClick={(e) => {
          type test = Expect<Equal<typeof e, React.MouseEvent<HTMLDivElement>>>;
        }}
      ></Wrapper>
    </>
  );
};

 

Option 2: A huge distrination union

export const Wrapper = <TAs extends keyof JSX.IntrinsicElements>(
  props: {
    as: TAs;
  } & ComponentProps<TAs>
) => {
  const Comp = props.as as string;

  return <Comp {...(props as any)}></Comp>;
};

 

Component `as` with Custom component

import React, { ElementType } from "react";
import { Equal, Expect } from "../helpers/type-utils";

export const Wrapper = <TAs extends ElementType>(
  props: {
    as: TAs;
  } & React.ComponentPropsWithoutRef<TAs>
) => {
  const Comp = props.as as string;

  return <Comp {...(props as any)}></Comp>;
};

const Custom = (props: { thisIsRequired: boolean }) => {
  return <a />;
};

const Example2 = () => {
  return (
    <>
      <Wrapper as={Custom} thisIsRequired />
      <Wrapper
        as={Custom}
        // @ts-expect-error incorrectProp should not be allowed
        incorrectProp
      />

      {/* @ts-expect-error thisIsRequired is not being passed */}
      <Wrapper as={Custom}></Wrapper>
    </>
  );
};

 

Utils function from React:

    /**
     * NOTE: prefer ComponentPropsWithRef, if the ref is forwarded,
     * or ComponentPropsWithoutRef when refs are not supported.
     */
    type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> =
        T extends JSXElementConstructor<infer P>
            ? P
            : T extends keyof JSX.IntrinsicElements
                ? JSX.IntrinsicElements[T]
                : {};
    type ComponentPropsWithRef<T extends ElementType> =
        T extends (new (props: infer P) => Component<any, any>)
            ? PropsWithoutRef<P> & RefAttributes<InstanceType<T>>
            : PropsWithRef<ComponentProps<T>>;
    type ComponentPropsWithoutRef<T extends ElementType> =
        PropsWithoutRef<ComponentProps<T>>;

 

Component `as` prop with default value

Solution 1: Problem for this solution is we lost the autocompletion for `as="button"`

import { ComponentPropsWithoutRef, ElementType } from "react";
import { Equal, Expect } from "../helpers/type-utils";

export const Link = <T extends ElementType = "a">(
  props: {
    as?: T;
  } & ComponentPropsWithoutRef<T>
) => {
  const { as: Comp = "a", ...rest } = props;
  return <Comp {...(rest as any)}></Comp>;
};

/**
 * Should work without specifying 'as'
 */

const Example1 = () => {
  return (
    <>
      <Link
        // @ts-expect-error doesNotExist is not a valid prop
        doesNotExist
      ></Link>

      <Link
        // e should be inferred correctly
        onClick={(e) => {
          type test = Expect<
            Equal<typeof e, React.MouseEvent<HTMLAnchorElement>>
          >;
        }}
      ></Link>
    </>
  );
};

/**
 * Should work specifying a 'button'
 */

const Example2 = () => {
  return (
    <>
      <Link
        as="button"
        // @ts-expect-error doesNotExist is not a valid prop
        doesNotExist
      ></Link>

      <Link
        as="button"
        // e should be inferred correctly
        onClick={(e) => {
          type test = Expect<
            Equal<typeof e, React.MouseEvent<HTMLButtonElement>>
          >;
        }}
      ></Link>
    </>
  );
};

/**
 * Should work with Custom components!
 */

const Custom = (
  props: { thisIsRequired: boolean },
  ref: React.ForwardedRef<HTMLAnchorElement>
) => {
  return <a ref={ref} />;
};

const Example3 = () => {
  return (
    <>
      <Link as={Custom} thisIsRequired />
      <Link
        as={Custom}
        // @ts-expect-error incorrectProp should not be allowed
        incorrectProp
      />

      {/* @ts-expect-error thisIsRequired is not being passed */}
      <Link as={Custom}></Link>
    </>
  );
};

 

 

Solution 2: This approach won't lost autocompletion 💯

export const Link = <T extends ElementType>(
  props: {
    as?: T;
  } & ComponentPropsWithoutRef<ElementType extends T ? "a" : T>
) => {
  const { as: Comp = "a", ...rest } = props;
  return <Comp {...(rest as any)}></Comp>;
};

 

The `as` props for `forwardRef` ✅

import { ComponentProps, ElementType, forwardRef, useRef } from "react";
import { Equal, Expect } from "../helpers/type-utils";

// Added fixedForwardRef from a previous exercise

type FixedForwardRef = <T, P = {}>(
  render: (props: P, ref: React.Ref<T>) => React.ReactNode
) => (props: P & React.RefAttributes<T>) => React.ReactNode;

const fixedForwardRef = forwardRef as FixedForwardRef;

// Added a DistributiveOmit type
type DistributiveOmit<T, TOmitted extends PropertyKey> = T extends any
  ? Omit<T, TOmitted>
  : never;

function UnwrappedLink<T extends ElementType>(
  props: {
    as?: T;
  } & DistributiveOmit<ComponentProps<ElementType extends T ? "a" : T>, "as">
) {
  const { as: Comp = "a", ...rest } = props;
  return <Comp {...rest}></Comp>;
}

const Link = fixedForwardRef(UnwrappedLink);

/**
 * Should work without specifying 'as'
 */

const Example1 = () => {
  const ref = useRef<HTMLAnchorElement>(null);
  const wrongRef = useRef<HTMLDivElement>(null);

  return (
    <>
      <Link ref={ref} />

      <Link
        // @ts-expect-error incorrect ref
        ref={wrongRef}
      />

      <Link
        // @ts-expect-error doesNotExist is not a valid prop
        doesNotExist
      ></Link>

      <Link
        // e should be inferred correctly
        onClick={(e) => {
          type test = Expect<
            Equal<typeof e, React.MouseEvent<HTMLAnchorElement>>
          >;
        }}
      ></Link>
    </>
  );
};

/**
 * Should work specifying a 'button'
 */

const Example2 = () => {
  const ref = useRef<HTMLButtonElement>(null);
  const wrongRef = useRef<HTMLSpanElement>(null);

  return (
    <>
      {/* CHECK ME! Check if autocomplete works on 'as' */}
      <Link as="button" />

      <Link as="button" ref={ref} />

      <Link
        as="button"
        // @ts-expect-error incorrect ref
        ref={wrongRef}
      />

      <Link
        as="button"
        // @ts-expect-error doesNotExist is not a valid prop
        doesNotExist
      ></Link>

      <Link
        as="button"
        // e should be inferred correctly
        onClick={(e) => {
          type test = Expect<
            Equal<typeof e, React.MouseEvent<HTMLButtonElement>>
          >;
        }}
      ></Link>
    </>
  );
};

/**
 * Should work with Custom components!
 */

const Custom = forwardRef(
  (
    props: { thisIsRequired: boolean },
    ref: React.ForwardedRef<HTMLAnchorElement>
  ) => {
    return <a ref={ref} />;
  }
);

const Example3 = () => {
  const ref = useRef<HTMLAnchorElement>(null);
  const wrongRef = useRef<HTMLDivElement>(null);
  return (
    <>
      <Link as={Custom} thisIsRequired />
      <Link
        as={Custom}
        // @ts-expect-error incorrectProp should not be allowed
        incorrectProp
      />

      {/* @ts-expect-error thisIsRequired is not being passed */}
      <Link as={Custom}></Link>

      <Link as={Custom} ref={ref} thisIsRequired />

      <Link
        as={Custom}
        // @ts-expect-error incorrect ref
        ref={wrongRef}
        thisIsRequired
      />
    </>
  );
};

 

posted @   Zhentiw  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
历史上的今天:
2022-08-31 [Typescript Challenges] 3. Easy - Tuple to Object
2022-08-31 [Typescript Challenges] 2. Easy -- readonly
2022-08-31 [Typescript Challenges] 1. Easy - Pick
2022-08-31 [Pattern] Flyweight
2021-08-31 [Cloud Architect] 3. Business Objectives
2021-08-31 [AWS] Launch configuration vs Launch template
2020-08-31 [Prostgres] Select Grouped and Aggregated Data with SQL
点击右上角即可分享
微信分享提示