You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React TypeScript:如何为组件插槽实现类型安全?

解决方案:实现Slot属性的类型安全合并

核心问题分析

原代码的类型定义存在两个关键缺陷:

  • ComponentWithSlots 直接合并默认root元素的props,无法根据用户传入的自定义slots.root动态更新属性类型
  • slotProps 绑定到固定的默认Slots类型,无法匹配用户实际传入的自定义Slot元素属性

重构类型定义

首先修正Slot和组件Props类型,让它们支持动态推断自定义Slot的属性:

import { ComponentPropsWithoutRef, ElementType, PropsWithChildren, ReactNode } from "react";

// 简化Slot类型,直接复用ElementType
export type Slot = ElementType;

// 重新定义带Slots的组件Props类型,支持动态推断
export type ComponentWithSlots<
  P,
  DefaultSlots extends Record<string, Slot>
> = PropsWithChildren<
  P & {
    // 允许覆盖默认Slot的元素类型
    slots?: Partial<{ [K in keyof DefaultSlots]: Slot }>;
    // 根据实际传入的Slot动态推导slotProps类型
    slotProps?: Partial<{
      [K in keyof DefaultSlots]: ComponentPropsWithoutRef<
        DefaultSlots[K] | (typeof slots)[K]
      >;
    }>;
  }
> & Omit<ComponentPropsWithoutRef<DefaultSlots["root"]>, keyof P | "children">;

调整Avatar组件实现

修改Avatar组件,让它能正确接收自定义Slots并推导对应属性:

import Image from "next/image";
import { cva } from "class-variance-authority";
import { twMerge } from "tailwind-merge";

const Variants = cva(
  "relative inline-flex items-center justify-center bg-black/30 dark:bg-white/30 backdrop-blur-lg",
  {
    variants: {
      size: {
        xs: "w-6 h-6",
        sm: "w-8 h-8",
        md: "w-10 h-10",
        lg: "w-12 h-12",
        xl: "w-14 h-14",
        "2xl": "w-16 h-16",
      },
      shape: {
        square: "rounded-none",
        rounded: "rounded-md",
        circular: "rounded-full",
      },
    },
  }
);

export interface AvatarProps {
  src?: string;
  alt?: string;
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  shape?: "square" | "rounded" | "circular";
  children?: ReactNode;
}

// 默认Slots定义
export type DefaultAvatarSlots = {
  root: "div";
  image: "img";
};

export function Avatar<CustomSlots extends Partial<DefaultAvatarSlots> = {}>({
  className,
  children,
  src,
  alt = "Avatar",
  size = "md",
  shape = "circular",
  slots,
  slotProps,
  ...rootProps
}: ComponentWithSlots<AvatarProps, DefaultAvatarSlots & CustomSlots>) {
  // 合并默认Slot与用户自定义Slot
  const resolvedSlots = {
    root: slots?.root ?? "div",
    image: slots?.image ?? "img",
  } as const;

  const Root = resolvedSlots.root;
  const ImageComponent = resolvedSlots.image;

  // 按优先级合并className:组件默认样式 > 用户传入className > slotProps中的className
  const rootClassNames = twMerge(
    Variants({ size, shape }),
    className,
    slotProps?.root?.className
  );
  const imageClassNames = twMerge(
    "h-full w-full object-cover",
    slotProps?.image?.className
  );

  // 传递props时避免冲突,最后覆盖className确保生效
  const filteredRootProps = {
    ...rootProps,
    ...slotProps?.root,
    className: rootClassNames,
  };

  return (
    <Root {...filteredRootProps}>
      {src ? (
        <ImageComponent
          src={src}
          alt={alt}
          {...slotProps?.image}
          className={imageClassNames}
        />
      ) : (
        children
      )}
    </Root>
  );
}

// 使用示例
export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-center">
      <Avatar
        src="/favicon.ico"
        alt="Logo"
        size="2xl"
        slots={{ image: Image }}
        slotProps={{ image: { fill: true, alt: "Custom Logo" } }}
      />
      {/* 测试自定义root元素 */}
      <Avatar
        shape="square"
        slots={{ root: "section" }}
        className="border border-red-500"
        children={<span>Placeholder</span>}
      />
    </main>
  );
}

关键改进点

  • 动态类型推断:ComponentWithSlots 会根据用户传入的slots自动推导slotProps和root元素的属性类型
  • className优先级控制:通过twMerge确保样式按「组件默认 > 用户传入 > SlotProps」的优先级合并
  • Props冲突规避:最后覆盖className,避免自定义样式被SlotProps覆盖
  • Children类型安全:显式关联Children类型与root元素的允许值,避免传入不兼容的子节点

内容的提问来源于stack exchange,提问作者Moussa Harajli

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 11:05:35