@axa-ch/react-polymorphic-types

中文文档
1.4.2发布于 2026/05/21MITTypeScriptES Module零依赖近 30 天下载 4014

About Zero-runtime polymorphic component definitions for React

原始使用文档
文档目录7 节
  1. About
  2. Explanation
  3. What is a React polymorphic component?
  4. What problems does this package solve?
  5. Installation and Usage
  6. Polymorphic Components Recipes
  7. Credits

react-polymorphic-types

Build Status MIT License NPM version NPM downloads

About

react-polymorphic-types is a library that enables the creation of zero-runtime polymorphic component definitions in React.

Explanation

What is a React polymorphic component?

When building design systems or reusable UI components in React, you may come across the need for polymorphic components. A polymorphic component is a versatile component that can render different underlying HTML elements or custom components based on a prop.

A React polymorphic component provides flexibility to the consumer, allowing them to specify the desired element or component type to be rendered using a prop.

For example, let's consider a polymorphic heading component:

import { createElement, ElementType, PropsWithChildren, ComponentProps } from 'react';

// Define the props for the polymorphic heading component
type HeadingProps<T extends ElementType = 'h1'> = PropsWithChildren<
  {
    as?: T;
  } & ComponentProps<T>
>;

// Define the polymorphic heading component
export const Heading = <T extends ElementType = 'h1'>({ as = 'h1', children, ...rest }: HeadingProps<T>) =>
  createElement(as, rest, children);

In the above example, the Heading component can render different heading levels (h1, h2, h3, etc.) based on the as prop. By default, it renders as an h1 element.

You can use the Heading component in your application like this:

const App = () => (
  <article>
    <Heading>My Main Headline</Heading>
    <Heading as='h2'>A Subtitle</Heading>
    <p>A description</p>
  </article>
);

In this case, the same Heading component is used to render two different semantic tags, h1 and h2, allowing you to control the heading level and maintain consistency across your application.

Polymorphic components provide an elegant solution for building flexible and reusable UI components in React, enabling you to create a cohesive design system with consistent semantics.

What problems does this package solve?

The use of the as attribute can become complex when adding constraints to your rendered markup or when using third-party components. Declaring polymorphic types for each component can also be a tedious task that you may want to abstract.

With @axa-ch/react-polymorphic-types, you can easily add constraints to your polymorphic React components and avoid redundant type definitions.

Installation and Usage

Install the TypeScript types via npm:

npm i @axa-ch/react-polymorphic-types -D

Polymorphic Components Recipes

The following recipes provide a starting point for creating polymorphic components. You can copy and modify them according to your requirements.

Basic Example

This example showcases a simple polymorphic heading element. It allows you to independently define its size and markup using props.

import { ComponentPropsWithoutRef, createElement, ElementType } from 'react';
import { PolymorphicProps } from '@axa-ch/react-polymorphic-types';

// Default HTML element if the "as" prop is not provided
export const HeadingDefaultElement: ElementType = 'h1';
// List of allowed HTML elements that can be passed via the "as" prop
export type HeadingAllowedElements = typeof HeadingDefaultElement | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export type HeadingSizes = 1 | 2 | 3 | 4 | 5 | 6;

// Component-specific props
export type HeadingOwnProps<T extends HeadingAllowedElements> = ComponentPropsWithoutRef<T> & {
  size?: HeadingSizes;
};

// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type HeadingProps<T extends HeadingAllowedElements = typeof HeadingDefaultElement> = PolymorphicProps<
  HeadingOwnProps<T>,
  T,
  HeadingAllowedElements
>;

export const Heading = <T extends HeadingAllowedElements>({
  as = HeadingDefaultElement,
  size,
  className,
  children,
  ...rest
}: HeadingProps<T>) =>
  createElement(
    as,
    {
      ...rest,
      className: `${className} size-${size || 1}`,
    },
    children,
  );

You can use the Heading component in your application as shown below:

const App = () => (
  <article>
    <Heading
      as='h1'
      size={2}
    >
      My Main Headline
    </Heading>
    <Heading
      as='h2'
      size={5}
    >
      A Subtitle
    </Heading>

    {/* The following component will throw a TypeScript error because 'div' elements are not allowed here */}
    <Heading
      as='div'
      size={5}
    >
      A Subtitle
    </Heading>
    <p>A description</p>
  </article>
);
Basic Example with Ref

This example is similar to the previous one, but it also allows the use of React refs.

import { ComponentPropsWithoutRef, createElement, ElementType, forwardRef } from 'react';
import { PolymorphicProps, PolymorphicForwardedRef } from '@axa-ch/react-polymorphic-types';

// Default HTML element if the "as" prop is not provided
export const HeadingDefaultElement: ElementType = 'h1';
// List of allowed HTML elements that can be passed via the "as" prop
export type HeadingAllowedElements = typeof HeadingDefaultElement | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export type HeadingSizes = 1 | 2 | 3 | 4 | 5 | 6;

// Component-specific props
export type HeadingOwnProps<T extends HeadingAllowedElements> = ComponentPropsWithoutRef<T> & {
  size?: HeadingSizes;
};

// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type HeadingProps<T extends HeadingAllowedElements> = PolymorphicProps<
  HeadingOwnProps<T>,
  T,
  HeadingAllowedElements
>;

const HeadingInner = <T extends HeadingAllowedElements>(
  { as = HeadingDefaultElement, size, className, children, ...rest }: HeaadingProps<T>,
  // notice the use of the PolymorphicForwardedRef type here
  ref: PolymorphicForwardedRef<T>,
) =>
  createElement(
    as,
    {
      ...rest,
      ref,
      className: `${className} size-${size || 1}`,
    },
    children,
  );

// Forward refs with generics is tricky
// see also https://fettblog.eu/typescript-react-generic-forward-refs/
export const Heading = forwardRef(HeadingInner) as unknown as <T extends HeadingAllowedElements>(
  props: HeadingProps<T> & { ref?: PolymorphicForwardedRef<T> },
) => ReturnType<typeof HeadingInner>;

Using the @axa-ch/react-polymorphic-types types will allow you to automatically infer the proper ref DOM node.

const App = () => {
  // The use of HTMLHeadingElement type is safe
  const ref = useRef<HTMLHeadingElement | null>(null);

  return (
    <Heading
      ref={ref}
      as='h2'
    />
  );
};
React 19 Example with Ref

This example is similar to the previous one, but with React 19 the refs forwarding got much easier

import { ComponentPropsWithoutRef, createElement, ElementType, forwardRef } from 'react';
import { PolymorphicProps, PolymorphicForwardedRef } from '@axa-ch/react-polymorphic-types';

// Default HTML element if the "as" prop is not provided
export const HeadingDefaultElement: ElementType = 'h1';
// List of allowed HTML elements that can be passed via the "as" prop
export type HeadingAllowedElements = typeof HeadingDefaultElement | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export type HeadingSizes = 1 | 2 | 3 | 4 | 5 | 6;

// Component-specific props
export type HeadingOwnProps<T extends HeadingAllowedElements> = ComponentPropsWithoutRef<T> & {
  size?: HeadingSizes;
  ref?: PolymorphicForwardedRef<T>;
};

// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type HeadingProps<T extends HeadingAllowedElements> = PolymorphicProps<
  HeadingOwnProps<T>,
  T,
  HeadingAllowedElements
>;

const Heading = <T extends HeadingAllowedElements>({
  as = HeadingDefaultElement,
  size,
  className,
  ref,
  children,
  ...rest
}: HeadingProps<T>) =>
  createElement(
    as,
    {
      ...rest,
      ref,
      className: `${className} size-${size || 1}`,
    },
    children,
  );

Using the @axa-ch/react-polymorphic-types types will allow you to automatically infer the proper ref DOM node.

const App = () => {
  // The use of HTMLHeadingElement type is safe
  const ref = useRef<HTMLHeadingElement | null>(null);

  return (
    <Heading
      ref={ref}
      as='h2'
    />
  );
};
Basic Example with Memo

This example shows the use of React.memo with a polymorphic component.

import { ComponentPropsWithoutRef, createElement, ElementType, memo } from 'react';
import { PolymorphicProps } from '@axa-ch/react-polymorphic-types';

// Default HTML element if the "as" prop is not provided
export const HeadingDefaultElement: ElementType = 'h1';
// List of allowed HTML elements that can be passed via the "as" prop
export type HeadingAllowedElements = typeof HeadingDefaultElement | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export type HeadingSizes = 1 | 2 | 3 | 4 | 5 | 6;

// Component-specific props
export type HeadingOwnProps<T extends HeadingAllowedElements> = ComponentPropsWithoutRef<T> & {
  size?: HeadingSizes;
};

// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type HeadingProps<T extends HeadingAllowedElements> = PolymorphicProps<
  HeadingOwnProps<T>,
  T,
  HeadingAllowedElements
>;

const HeadingInner = <T extends HeadingAllowedElements>({
  as = HeadingDefaultElement,
  size,
  className,
  children,
  ...rest
}: HeadingProps<T>) =>
  createElement(
    as,
    {
      ...rest,
      className: `${className} size-${size || 1}`,
    },
    children,
  );

// Memo with generics is tricky
// see also https://fettblog.eu/typescript-react-generic-forward-refs/
export const Heading = memo(HeadingInner) as <T extends HeadingAllowedElements>(
  props: HeadingProps<T>,
) => ReturnType<typeof HeadingInner>;

The above component can be consumed without any additional overhead as follows:

const App = () => (
  <>
    <Heading as='h2' />
  </>
);
Exotic Component Example

Polymorphic exotic components allow you to use either DOM nodes or custom rendering functions for your HTML.

import { ComponentPropsWithoutRef, createElement, ElementType, ExoticComponent } from 'react';
import { PolymorphicExoticProps, PolymorphicProps } from '@axa-ch/react-polymorphic-types';

// Default HTML element if the "as" prop is not provided
export const ContainerDefaultElement: ElementType = 'div';
// List of allowed HTML elements that can be passed via the "as" prop
export type ContainerAllowedDOMElements = typeof ContainerDefaultElement | 'article' | 'section';
export type ContainerAllowedElements = ContainerAllowedDOMElements | ExoticComponent;

// Component-specific props
export type ContainerOwnProps<T extends ContainerAllowedDOMElements> = ComponentPropsWithoutRef<T>;

// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type ContainerProps<T extends ContainerAllowedElements> = T extends ContainerAllowedDOMElements
  ? PolymorphicProps<ContainerOwnProps<T>, T, ContainerAllowedDOMElements>
  : PolymorphicExoticProps<ContainerOwnProps<ContainerAllowedDOMElements>, T, ContainerAllowedDOMElements>;

export const Container = <T extends ContainerAllowedElements>({
  as = ContainerDefaultElement,
  className,
  children,
  ...rest
}: ContainerProps<T>) =>
  createElement(
    as,
    {
      ...rest,
      className,
    },
    children,
  );

The above component works with straight HTML nodes or with external exotic components like, for example, the ones provided by framer-motion.

import { motion } from 'framer-motion';

const App = () => (
  <>
    <Container as='div' />
    {/* Notice that the exotic props here will be automatically inferred */}
    <Container
      as={motion.article}
      layout
    />
  </>
);
Exotic Component Example with Ref

Polymorphic exotic components that use refs are slightly more complex and require some additional code to work properly.

import { ComponentPropsWithoutRef, createElement, ElementType, ExoticComponent, forwardRef } from 'react';
import { PolymorphicProps, PolymorphicForwardedRef, PolymorphicExoticProps } from '@axa-ch/react-polymorphic-types';

// Default HTML element if the "as" prop is not provided
export const ContainerDefaultElement: ElementType = 'div';
// List of allowed HTML elements that can be passed via the "as" prop
export type ContainerAllowedDOMElements = 'div' | 'article' | 'section';
export type ContainerAllowedElements = ContainerAllowedDOMElements | ExoticComponent;

// Component-specific props
export type ContainerOwnProps<T extends ContainerAllowedDOMElements> = ComponentPropsWithoutRef<T>;

// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type ContainerProps<T extends ContainerAllowedElements> = T extends ContainerAllowedDOMElements
  ? PolymorphicProps<ContainerOwnProps<T>, T, ContainerAllowedDOMElements>
  : PolymorphicExoticProps<ContainerOwnProps<ContainerAllowedDOMElements>, T, ContainerAllowedDOMElements>;

// Forwarded ref component
const ContainerInner = <T extends ContainerAllowedElements>(
  { as = ContainerDefaultElement, className, children, ...rest }: ContainerProps<T>,
  ref: PolymorphicForwardedRef<T>,
) =>
  createElement(
    as,
    {
      ...rest,
      ref,
      className,
    },
    children,
  );

// Forward refs with generics is tricky
// see also https://fettblog.eu/typescript-react-generic-forward-refs/
export const Container = forwardRef<ContainerAllowedElements>(ContainerInner) as <T extends ContainerAllowedElements>(
  props: ContainerProps<T> & { ref?: PolymorphicForwardedRef<T> },
) => ReturnType<typeof ContainerInner>;

With the above example, DOM nodes will be automatically inferred, including when using third-party exotic rendering functions.

import { motion } from 'framer-motion';

const App = () => {
  const div = useRef<HTMLDivElement | null>(null);
  // Article and other HTML5 tags are just of type HTMLElement
  const article = useRef<HTMLElement | null>(null);

  return (
    <>
      <Container
        ref={div}
        as='div'
      />
      <Container
        ref={article}
        as={motion.article}
        layout
      />
    </>
  );
};
Complex Exotic/Functional Component Example

This example combines multiple rendering strategies for your component to allow maximum flexibility for its consumers.

// We need to infer the functional component properties so 'any' is used in this case
// You can also add strict types for your functional components, but it will reduce flexibility
import { ComponentPropsWithoutRef, createElement, ElementType, ExoticComponent, FC } from 'react';
import { PolymorphicFunctionalProps, PolymorphicExoticProps, PolymorphicProps } from '@axa-ch/react-polymorphic-types';

// Default HTML element if the "as" prop is not provided
export const ContainerDefaultElement: ElementType = 'div';
// List of allowed HTML elements that can be passed via the "as" prop
export type ContainerAllowedDOMElements = 'div' | 'article' | 'section';
export type ContainerAllowedElements = ContainerAllowedDOMElements | ExoticComponent | FC<any>;

// Component-specific props
export type ContainerOwnProps<T extends ContainerAllowedDOMElements> = ComponentPropsWithoutRef<T>;

// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type ContainerProps<T extends ContainerAllowedElements> = T extends ContainerAllowedDOMElements
  ? PolymorphicProps<ContainerOwnProps<T>, T, ContainerAllowedDOMElements>
  : T extends FC<any>
    ? PolymorphicFunctionalProps<ContainerOwnProps<ContainerAllowedDOMElements>, T, ContainerAllowedDOMElements>
    : PolymorphicExoticProps<ContainerOwnProps<ContainerAllowedDOMElements>, T, ContainerAllowedDOMElements>;

export const Container = <T extends ContainerAllowedElements>({
  as = ContainerDefaultElement,
  className,
  children,
  ...rest
}: ContainerProps<T>) =>
  createElement(
    as,
    {
      ...rest,
      className,
    },
    children,
  );

Let's see how we can use the above component with all its possible rendering options:

import { motion } from 'framer-motion';

type FooProps = ComponentPropsWithoutRef<'div'> & { size: 'small' | 'large'; name: string };

const Foo: FC<FooProps> = ({ className, size = 'large', ...rest }) => (
  <div
    {...rest}
    className={`${className} the-foo ${size}`}
  />
);

const App = () => (
  <>
    <Container as='div' />
    <Container
      size='small'
      name='foo'
      as={Foo}
    />
    <Container
      as={motion.div}
      layout
      animate
    />
  </>
);

Credits

This project wouldn't exist without react-polymorphic-types

版本与发布记录

@axa-ch/react-polymorphic-types 共发布 16 个版本,首次发布于 2023/06/14;过去 12 个月发布了 1 个版本。

版本发布日期距今
1.4.2latest2026/05/215 个月前
1.4.12025/08/201.2 年前
1.4.02025/05/201.5 年前
1.3.12025/05/201.5 年前
1.3.02025/01/071.8 年前
1.2.62024/10/102 年前

关于 @axa-ch/react-polymorphic-types 的常见问题

@axa-ch/react-polymorphic-types 是什么?

About Zero-runtime polymorphic component definitions for React

如何安装 @axa-ch/react-polymorphic-types?

在项目目录运行 npm install @axa-ch/react-polymorphic-types,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^18.0.0 || ^19.0.0)、@types/react(^18.0.0 || ^19.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@axa-ch/react-polymorphic-types 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@axa-ch/react-polymorphic-types 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@axa-ch/react-polymorphic-types 可以商用吗?

@axa-ch/react-polymorphic-types 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@axa-ch/react-polymorphic-types 的下载量有多少?

根据 npm 官方下载统计,@axa-ch/react-polymorphic-types 在 2026/10/08 – 2026/11/06 这 30 天内被下载 4,014 次,日均约 134 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@axa-ch/react-polymorphic-types 还在维护吗?

最新版本 1.4.2 发布于 2026/05/21(5 个月前);至今共发布 16 个版本,其中过去 12 个月发布了 1 个;首次发布于 2023/06/14。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。