React TypeScript: 具有类型安全性的组件插槽

huangapple go评论83阅读模式
英文:

React TypeScript: Component Slots with Type-Safety

问题

以下是您要翻译的代码部分:

我有一个关于组件 API 的工作示例我正试图实现它然而我无法正确获得 children  className 属性的类型安全性这是我的组件

import Image from "next/image";
import { cva } from "class-variance-authority";
import {
  ComponentPropsWithoutRef,
  ElementType,
  PropsWithChildren,
} from "react";
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 type Slot<T extends ElementType = ElementType> =
  T extends infer U extends string ? U : T;

export type ComponentWithSlots<
  P,
  S extends Record<string, Slot>
> = PropsWithChildren<
  P & {
    slots?: { [K in keyof S]?: ElementType };
    slotProps?: { [K in keyof S]?: ComponentPropsWithoutRef<S[K]> };
  }
> &
  ComponentPropsWithoutRef<S["root"]>;

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

export type AvatarSlots = {
  root: Slot<"div">;
  image: Slot<"img">;
};

export function Avatar<T extends AvatarSlots = AvatarSlots>({
  className,
  children,
  src,
  alt = "Avatar",
  size = "md",
  shape = "circular",
  slots,
  slotProps,
  ...props
}: ComponentWithSlots<AvatarProps, T>) {
  const Root = slots?.root ?? "div";
  const Image = slots?.image ?? "img";

  const rootClassNames = twMerge(Variants({ size, shape }), className);
  const imageClassNames = twMerge(
    "h-full w-full object-cover",
    slotProps?.image?.className
  );

  return (
    <Root {...props} {...slotProps?.root} className={rootClassNames}>
      {src ? (
        <Image
          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: { layout: "fill" } }}
      />
    </main>
  );
}

请注意,上述代码已经被翻译成中文。如果您有任何其他问题或需要进一步的帮助,请告诉我。

英文:

I have a working example of a component API I'm trying to achieve. However I'm not able to properly get type-safety for the children and className prop. Here's my component:

import Image from &quot;next/image&quot;;
import { cva } from &quot;class-variance-authority&quot;;
import {
ComponentPropsWithoutRef,
ElementType,
PropsWithChildren,
} from &quot;react&quot;;
import { twMerge } from &quot;tailwind-merge&quot;;
const Variants = cva(
&quot;relative inline-flex items-center justify-center bg-black/30 dark:bg-white/30 backdrop-blur-lg&quot;,
{
variants: {
size: {
xs: &quot;w-6 h-6&quot;,
sm: &quot;w-8 h-8&quot;,
md: &quot;w-10 h-10&quot;,
lg: &quot;w-12 h-12&quot;,
xl: &quot;w-14 h-14&quot;,
&quot;2xl&quot;: &quot;w-16 h-16&quot;,
},
shape: {
square: &quot;rounded-none&quot;,
rounded: &quot;rounded-md&quot;,
circular: &quot;rounded-full&quot;,
},
},
}
);
export type Slot&lt;T extends ElementType = ElementType&gt; =
T extends infer U extends string ? U : T;
export type ComponentWithSlots&lt;
P,
S extends Record&lt;string, Slot&gt;
&gt; = PropsWithChildren&lt;
P &amp; {
slots?: { [K in keyof S]?: ElementType };
slotProps?: { [K in keyof S]?: ComponentPropsWithoutRef&lt;S[K]&gt; };
}
&gt; &amp;
ComponentPropsWithoutRef&lt;S[&quot;root&quot;]&gt;;
export interface AvatarProps {
src?: string;
alt?: string;
size?: &quot;xs&quot; | &quot;sm&quot; | &quot;md&quot; | &quot;lg&quot; | &quot;xl&quot; | &quot;2xl&quot;;
shape?: &quot;square&quot; | &quot;rounded&quot; | &quot;circular&quot;;
}
export type AvatarSlots = {
root: Slot&lt;&quot;div&quot;&gt;;
image: Slot&lt;&quot;img&quot;&gt;;
};
export function Avatar&lt;T extends AvatarSlots = AvatarSlots&gt;({
className,
children,
src,
alt = &quot;Avatar&quot;,
size = &quot;md&quot;,
shape = &quot;circular&quot;,
slots,
slotProps,
...props
}: ComponentWithSlots&lt;AvatarProps, T&gt;) {
const Root = slots?.root ?? &quot;div&quot;;
const Image = slots?.image ?? &quot;img&quot;;
const rootClassNames = twMerge(Variants({ size, shape }), className);
const imageClassNames = twMerge(
&quot;h-full w-full object-cover&quot;,
slotProps?.image?.className
);
return (
&lt;Root {...props} {...slotProps?.root} className={rootClassNames}&gt;
{src ? (
&lt;Image
src={src}
alt={alt}
{...slotProps?.image}
className={imageClassNames}
/&gt;
) : (
children
)}
&lt;/Root&gt;
);
}
export default function Home() {
return (
&lt;main className=&quot;flex min-h-screen flex-col items-center justify-center&quot;&gt;
&lt;Avatar
src=&quot;/favicon.ico&quot;
alt=&quot;Logo&quot;
size=&quot;2xl&quot;
slots={{ image: Image }}
slotProps={{ image: { layout: &quot;fill&quot; } }}
/&gt;
&lt;/main&gt;
);
}

I want to merge the props of the 'Slot' that gets inferred, this can be any HTML element.What might be the best approach for this?

CodeSandbox Link

答案1

得分: 1

以下是翻译好的代码部分:

import Image from "next/image";
import { cva } from "class-variance-authority";
import {
ComponentPropsWithoutRef,
ElementType,
PropsWithChildren,
} from "react";
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 type Slot<T extends ElementType = ElementType> =
T extends infer U extends string ? U : T;
export type ComponentWithSlots<
P,
S extends Record<string, Slot>
> = PropsWithChildren<
P & {
// Change from ElementType to S[K]
slots?: { [K in keyof S]?: S[K] };
slotProps?: {
// Omit the AvatarProps from the slot
[K in keyof S]?: Omit<ComponentPropsWithoutRef<S[K]>, keyof Required<P>>;
};
}
> &
ComponentPropsWithoutRef<S["root"]>;
// I don't think src, and alt should be optional
export interface AvatarProps {
src?: string;
alt?: string;
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
shape?: "square" | "rounded" | "circular";
}
// Make AvatarSlots generic
export type AvatarSlots<R extends ElementType, I extends ElementType> = {
root: Slot<R>;
image: Slot<I>;
};
// Add generics correctly to be able to infer from the return type
export function Avatar<
RootEl extends ElementType = "div",
ImgEl extends ElementType = "img"
>({
className,
children,
src,
alt = "Avatar",
size = "md",
shape = "circular",
slots,
slotProps,
...props
}: ComponentWithSlots<AvatarProps, AvatarSlots<RootEl, ImgEl>>) {
const Root = slots?.root ?? "div";
const Image = slots?.image ?? "img";
const rootClassNames = twMerge(Variants({ size, shape }), className);
const imageClassNames = twMerge(
"h-full w-full object-cover",
slotProps?.image?.className
);
return (
<Root {...props} {...slotProps?.root} className={rootClassNames}>
{src ? (
<Image
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: { layout: "fill" } }}
/>
</main>
);
}

希望这有所帮助!如果您需要进一步的翻译或有其他问题,请随时提问。

英文:

Here's how you can achieve what you're trying to do -

import Image from &quot;next/image&quot;;
import { cva } from &quot;class-variance-authority&quot;;
import {
ComponentPropsWithoutRef,
ElementType,
PropsWithChildren,
} from &quot;react&quot;;
import { twMerge } from &quot;tailwind-merge&quot;;
const Variants = cva(
&quot;relative inline-flex items-center justify-center bg-black/30 dark:bg-white/30 backdrop-blur-lg&quot;,
{
variants: {
size: {
xs: &quot;w-6 h-6&quot;,
sm: &quot;w-8 h-8&quot;,
md: &quot;w-10 h-10&quot;,
lg: &quot;w-12 h-12&quot;,
xl: &quot;w-14 h-14&quot;,
&quot;2xl&quot;: &quot;w-16 h-16&quot;,
},
shape: {
square: &quot;rounded-none&quot;,
rounded: &quot;rounded-md&quot;,
circular: &quot;rounded-full&quot;,
},
},
}
);
export type Slot&lt;T extends ElementType = ElementType&gt; =
T extends infer U extends string ? U : T;
export type ComponentWithSlots&lt;
P,
S extends Record&lt;string, Slot&gt;
&gt; = PropsWithChildren&lt;
P &amp; {
// Change from ElementType to S[K]
slots?: { [K in keyof S]?: S[K] };
slotProps?: {
// Omit the AvatarProps from the slot
[K in keyof S]?: Omit&lt;ComponentPropsWithoutRef&lt;S[K]&gt;, keyof Required&lt;P&gt;&gt;;
};
}
&gt; &amp;
ComponentPropsWithoutRef&lt;S[&quot;root&quot;]&gt;;
// I don&#39;t think src, and alt should be optional
export interface AvatarProps {
src?: string;
alt?: string;
size?: &quot;xs&quot; | &quot;sm&quot; | &quot;md&quot; | &quot;lg&quot; | &quot;xl&quot; | &quot;2xl&quot;;
shape?: &quot;square&quot; | &quot;rounded&quot; | &quot;circular&quot;;
}
// Make AvatarSlots generic
export type AvatarSlots&lt;R extends ElementType, I extends ElementType&gt; = {
root: Slot&lt;R&gt;;
image: Slot&lt;I&gt;;
};
// Add generics correctly to be able to infer from the return type
export function Avatar&lt;
RootEl extends ElementType = &quot;div&quot;,
ImgEl extends ElementType = &quot;img&quot;
&gt;({
className,
children,
src,
alt = &quot;Avatar&quot;,
size = &quot;md&quot;,
shape = &quot;circular&quot;,
slots,
slotProps,
...props
}: ComponentWithSlots&lt;AvatarProps, AvatarSlots&lt;RootEl, ImgEl&gt;&gt;) {
const Root = slots?.root ?? &quot;div&quot;;
const Image = slots?.image ?? &quot;img&quot;;
const rootClassNames = twMerge(Variants({ size, shape }), className);
const imageClassNames = twMerge(
&quot;h-full w-full object-cover&quot;,
slotProps?.image?.className
);
return (
&lt;Root {...props} {...slotProps?.root} className={rootClassNames}&gt;
{src ? (
&lt;Image
src={src}
alt={alt}
{...slotProps?.image}
className={imageClassNames}
/&gt;
) : (
children
)}
&lt;/Root&gt;
);
}
export default function Home() {
return (
&lt;main className=&quot;flex min-h-screen flex-col items-center justify-center&quot;&gt;
&lt;Avatar
src=&quot;/favicon.ico&quot;
alt=&quot;Logo&quot;
size=&quot;2xl&quot;
slots={{ image: Image }}
slotProps={{ image: { layout: &quot;fill&quot; } }}
/&gt;
&lt;/main&gt;
);
}

Here's a Codesandbox link.

NOTE: Since you've added the src and alt props in the Avatar component, I'd to make an adjustment while inferring the props from the slot, in particular, the Image component that you're using has a required prop of src and alt, so I'd to Omit the AvatarProps from the props of whatever slot that's chosen, and that's how it should be actually. One shouldn't be prompted to provide src and alt in the slotProps. My recommendation is to make src and alt required in AvatarProps.

答案2

得分: 0

只需将其添加到类型定义中:

import { PropsWithChildren } from "react";

export function Avatar<T extends Record<string, Slot> = AvatarSlots>({
  className,
  children,
  src,
  alt = "Avatar",
  size = "md",
  shape = "circular",
  slots,
  slotProps,
  ...props
  // add this:
}: PropsWithChildren<ComponentWithSlots<AvatarProps, T>> & {
  className?: string;
}) {
  const Root = slots?.root ?? "div";
  const Image = slots?.image ?? "img";

  const rootClassNames = twMerge(Variants({ size, shape }), className);
  const imageClassNames = twMerge(
    "h-full w-full object-cover",
    slotProps?.image?.className
  );

  return (
    <Root {...props} {...slotProps?.root} className={rootClassNames}>
      {src ? (
        <Image
          src={src}
          alt={alt}
          {...slotProps?.image}
          className={imageClassNames}
        />
      ) : (
        children
      )}
    </Root>
  );
}

您还可以将其提取到一个单独的类型别名中。

英文:

Just add it to the type definition:

import { PropsWithChildren } from &quot;react&quot;;
export function Avatar&lt;T extends Record&lt;string, Slot&gt; = AvatarSlots&gt;({
className,
children,
src,
alt = &quot;Avatar&quot;,
size = &quot;md&quot;,
shape = &quot;circular&quot;,
slots,
slotProps,
...props
// add this:
}: PropsWithChildren&lt;ComponentWithSlots&lt;AvatarProps, T&gt;&gt; &amp; {
className?: string;
}) {
const Root = slots?.root ?? &quot;div&quot;;
const Image = slots?.image ?? &quot;img&quot;;
const rootClassNames = twMerge(Variants({ size, shape }), className);
const imageClassNames = twMerge(
&quot;h-full w-full object-cover&quot;,
slotProps?.image?.className
);
return (
&lt;Root {...props} {...slotProps?.root} className={rootClassNames}&gt;
{src ? (
&lt;Image
src={src}
alt={alt}
{...slotProps?.image}
className={imageClassNames}
/&gt;
) : (
children
)}
&lt;/Root&gt;
);
}

You can also extract it to a separate type alias.

huangapple
  • 本文由 发表于 2023年7月31日 23:05:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/76804903.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定