英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论