react props的唯一联合



我试图为react组件道具定义一个typescript类型。我的组件是一个接受icon道具或text道具的基本按钮。不能两者都有,但必须有一个。

我试图从一个基本的歧视联合开始,但它不像预期的那样工作:

interface TextButtonProps extends TypedButtonProps {
text: string
}
interface IconButtonProps extends TypedButtonProps {
icon: JSX.Element
}
export const Button = ({ onClick, ...props }: IconButtonProps | TextButtonProps): JSX.Element => {
//...

当我在其他地方使用该组件时,TS不会抛出任何错误:

<Button icon={<IconClose />} text='test' uiVariant='default' />

根据我在网上找到的一篇文章,描述带有可选属性和的接口永远不会起作用:
interface TextButtonProps extends TypedButtonProps {
text?: string
icon?: never
}
interface IconButtonProps extends TypedButtonProps {
icon?: JSX.Element
text?: never
}

如果icontext同时存在,我对<Button>的所有使用都会抛出错误。

为什么会这样?我不喜欢它的冗长——如果我添加更多的按钮类型,我必须为每个界面添加这些新属性。

我的第二个问题是,因为属性是可选的,我可以不定义图标或文本道具-记住我需要确保其中一个存在。

是否有更干净的解决方案可以满足我的需求?

你几乎就在那里,需要一个而不是两个,只标记never属性可选:

interface TextButtonProps extends TypedButtonProps {
text: string
icon?: never
}
interface IconButtonProps extends TypedButtonProps {
icon: JSX.Element
text?: never
}

如果TS像Flow一样添加了对精确对象类型的支持,那么也许你可以在没有可选的never道具的情况下获得你想要的错误。但是对于确切类型的特性请求已经开放了很长时间…

另一种选择是创建单独的组件,这些组件委托给底层的Button:

export const IconButton = (props: IconButtonProps) => <Button {...props} />
export const TextButton = (props: TextButtonProps) => <Button {...props} />

那么TS的多余属性检查在大多数情况下是有效的。重要的是要记住,多余的属性检查并不是完美的,它不能保证像精确的对象类型那样可靠。

鉴别联合在这里可能是误导:这样的鉴别联合是基于鉴别属性的,它接受一个唯一的文字值,联合中的每个类型一个值,这样就可以明确地处理哪个类型,如vighnesh153的回答所示。

但是这里你不需要这样的判别性质。


为什么会这样?

任何类型都不能赋值给never,因此尝试用任何类型(neverundefined除外)定义prop/key是错误的。

这有效地禁止传递带有该名称的已定义prop/key。


因为属性是可选的,所以我可以不定义图标或文本道具

正如Andy的回答所解释的,只有禁止的never类型属性应该是可选的。您可以保留所需的其他属性。


它是多么冗长-如果我添加更多的按钮类型,我必须添加这些新属性到每个界面。

我们可以创建一些helper类型来分解代码。我猜这就是LindaPaiste在他们对这个问题的评论中提到的,我将让他们描述他们自己的解决方案,因为这听起来是个有趣的通用助手!

同时,针对你的情况,你可以先建立一个禁止所有特定道具的类型,这样你就可以在每个按钮类型上使用它,省略特定道具,使它们成为合法的:

// List all specific props to be forbidden
type ButtonSpecificProps = 'text' | 'icon' | 'other' | 'otherCombined' | 'otherOptional'
// Mapped type to convert the list (union of literals) into forbidden keys of an object
type NeverButtonSpecificProps = {
//^? { text?: undefined; icon?: undefined; other?: undefined; otherCombined?: undefined; otherOptional?: undefined }
[P in ButtonSpecificProps]?: never
}
type OnlyButtonProps<T> = T // Legal specific props
& Omit<NeverButtonSpecificProps, keyof T> // Forbidden props, except for legal ones
& TypedButtonProps // Other common props
type TextButtonProps = OnlyButtonProps<{
text: string
}>
type IconButtonProps = OnlyButtonProps<{
icon: JSX.Element
}>
// You can have several legal props simultaneously, possibly some optional
type OtherButtonProps = OnlyButtonProps<{
other: string
otherCombined: boolean
otherOptional?: number
}>
export const Button = ({ onClick, ...props }: IconButtonProps | TextButtonProps | OtherButtonProps) => {
//...
return null
}
() => (
<>
{/* Types of property 'text' are incompatible.
Type 'string' is not assignable to type 'undefined'. */}
<Button icon={<div />} text='test' uiVariant='default' />
{/* Type '{}' is not assignable to type 'IntrinsicAttributes & (TextButtonProps | IconButtonProps | OtherButtonProps)'. */}
<Button />
<Button text='foo' />
<Button icon={<div />} />
{/* Property 'otherCombined' is missing in type '{ other: string; }' but required in type '{ other: string; otherCombined: boolean; otherOptional?: number | undefined; }'. */}
<Button other='foo' />
<Button other='foo' otherCombined={true} />
<Button other='foo' otherCombined={true} otherOptional={2} />
</>
)

操场上联系

您必须在两个接口中包含一个公共属性,其值在每个接口中都是不同的。了解更多关于鉴别联合的信息

interface ButtonProps {
onClick?: (e: Event) => void;
}
interface IconButtonProps extends ButtonProps {
type: 'icon-button';
icon: React.ReactElement;
}
interface TextButtonProps extends ButtonProps {
type: 'text-button';
text: string;
}
function Button(props: IconButtonProps | TextButtonProps) {
return null
}
export default function App() {
return (
<div>
<Button type="icon-button" icon={<div />} />
<Button type="text-button" text="text" />
</div>
);
}

最新更新