英文:
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 "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 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?
答案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 "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 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 "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>
);
}
You can also extract it to a separate type alias.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论