reactjs 恰好一个/React支柱的区分联合

snz8szmq  于 2023-01-12  发布在  React
关注(0)|答案(4)|浏览(113)

我正在尝试为react组件属性定义一个类型脚本。我的组件是一个基本按钮,可以接受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' />
我在网上找到一篇文章,描述了带有可选属性的接口,但 never works:

interface TextButtonProps extends TypedButtonProps {
  text?: string
  icon?: never
}

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

如果icontext同时存在,我对<Button>的所有使用都会抛出错误。
为什么会这样呢?我对它的冗长并不感到兴奋--如果我添加更多的按钮类型,我必须将这些新属性添加到每个界面中。
我的第二个问题是,因为属性是 optional,我可以不定义**图标或文本 prop -记住,我需要确保一个或另一个存在。
是否有更清洁的解决方案可以满足我的需求?

wnrlj8wa

wnrlj8wa1#

您几乎已经完成了,如果需要一个而不是两个,只需将never属性标记为optional:

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的额外属性检查就可以工作了--在大多数情况下。重要的是要记住额外属性检查并不完美,它不能像精确对象类型那样保证可靠性。

zz2j4svz

zz2j4svz2#

  • 歧视性结合 * 在这里可能是误导:这样的区分联合体基于 discriminant property,其采用 unique literal 值,联合体中的每种类型一个值,从而使其显式地表明您处理的是哪种类型,如vighnesh153's answer中所示。

但是在这里你不想要这样的区别性属性。
为什么会这样?
没有类型可分配给never,因此尝试用任何类型(neverundefined以外的类型)定义属性/键都是错误的。
这有效地禁止传递具有该名称的已定义属性/键。
因为属性是可选的,所以我可以不定义图标或文本属性
正如Andy's answer中所解释的,只有被禁止的never类型属性应该是可选的,其他属性可以保留为必需的。
它是多么冗长-如果我添加更多按钮类型,我必须将那些新属性添加到每个单独的接口。
我们可以创建一些helper类型来分解代码。我猜这就是LindaPaiste在他们对这个问题的评论中提到的,我会让他们描述自己的解决方案,因为这听起来是一个有趣的通用helper!
与此同时,针对您的具体情况,您可以首先构建一个禁止 * 所有 * 特定 prop 的类型,这样您就可以在每个按钮类型上使用它,省略特定 prop ,使它们成为法律的的:

// 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} />
    </>
)

Playground链接

eiee3dmh

eiee3dmh3#

您必须在两个接口中包含一个公共属性,该属性的值在每个接口中都不同。了解有关区分联合的详细信息

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>
  );
}
k5hmc34c

k5hmc34c4#

你不需要定义两个接口来设置你的组件将接收到的值,一个接口就足够了,声明你需要什么样的参数,如果图标是字符串类型的名称,也是字符串类型按钮的标题;然后声明不需要的值并在组件中初始化它们。

`import React from 'react';
import { Text, TouchableOpacity, StyleSheet } from 'react-native';
import { COLORS, FONTS } from '../theme/constants';

interface Props {
    title: string | JSX.Element;
    textColor?: string;
    bgColor?: string;
    onPress?: () => void;
}

const CustomButton = ({ title, textColor = COLORS.white, bgColor = COLORS.primary, onPress }: Props) => {
    return (
        <TouchableOpacity
            style={{
                ...styles.buttonLogin,
                backgroundColor: bgColor,
            }}
            onPress={onPress}
            activeOpacity={0.8}
        >
            <Text style={{
                ...styles.textButtonLogin,
                color: textColor,
                ...FONTS.body3,
            }}>
                {title}
            </Text>
        </TouchableOpacity>
    );
};

const styles = StyleSheet.create({
    buttonLogin: {
        backgroundColor: COLORS.primary,
        padding: 15,
        width: '90%',
        borderRadius: 20,
        marginTop: 30,
        alignItems: 'center',
        margin: 10,
    },

    textButtonLogin: {
        fontSize: 15,
        fontWeight: 'bold',
        color: COLORS.white,
    },
});
export default CustomButton;`

相关问题