发布于 2026-01-06 4 阅读
0

使用 TypeScript 实现条件 React props DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

使用 TypeScript 实现 React 条件 props

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

仅当另一个属性具有特定值时才应设置的属性。

React 组件 props 之间的关系可能会让你感到棘手。本文将指导你如何使用 TypeScript 实现条件 props 模式。我将提出不同的场景,并演示以下问题的答案:

如何使用 TypeScript 创建多个属性之间的依赖关系?

我们该如何让它在关系断开时生成 TypeScript 错误?

相互冲突的属性

在开发设计系统时,我需要创建一个头像组件。要将 props 传递给头像组件,需要考虑以下几个条件:

  • 如果我传递icon属性,我就无法传递src属性
  • 如果我传递src属性,我就无法传递icon属性

这里提供一个不带条件的简单头像组件示例。

type AvatarProps = {
  icon?: JSX.Element;
  src?: string;
  children:React.ReactNode;
};

export const Avatar = (props: AvatarProps): JSX.Element => {
  const { icon, src } = props;
  return (
    <div>
      {icon && icon}
      {JSON.stringify(src)}
      {children}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

如果我们在导入组件时同时传递这两个属性,该组件就不会引发任何错误。

因此,我们必须向开发者提供提示,告诉他们同时传递这两个参数是被禁止的,只需抛出一个 TypeScript 错误即可。

为了实现这一点,我们可以使用两种类型创建联合类型,这两种类型分别反映了我们的组件支持的两种场景:

interface CommonProps {
  children?: React.ReactNode

  // ...other props that always exist
}

type ConditionalProps =
  | {
      icon?: JSX.Element;
      src?: never;
    }
  | {
      icon?: never;
      src?: string;
    };

type Props = CommonProps & ConditionalProps  

export const Avatar = (props: Props): JSX.Element => {
  const { icon, src } = props;
  return (
    <div>
      {icon && icon}
      {JSON.stringify(src)}
      {children}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

对于已经熟悉 TypeScript 的人来说,这些信息应该足够了。

然而,短短几行代码却包含了很多功能。如果你想了解这些代码的含义和工作原理,让我们把它分解成几个部分来讲解。

interface CommonProps {
  children: React.ReactNode

  // ...other props that always exist
}
Enter fullscreen mode Exit fullscreen mode

CommonProps这是 TypeScript 中典型的 props 定义。它用于定义所有“通用”props,这些props 在所有场景中都会出现,并且不依赖于其他 props。此外,可能children,还有 ` <props>`、`<props>` `<props>` 等。shadowsizeshape

type ConditionalProps =
// If i pass the icon prop i can't pass the src prop
  | {
      icon?: JSX.Element;
      src?: never;
    }
// If i pass the src prop i can't pass the icon prop
  | {
      src?: string;
      icon?: never;
    };
Enter fullscreen mode Exit fullscreen mode

ConditionalProps这就是奇迹发生的地方。它被称为“区别性联合”。它是对象定义的联合。

让我们进一步分析,然后再来看看这个受歧视的工会是如何为我们服务的。

{
 icon?: JSX.Element;
 src?: never;
} 
Enter fullscreen mode Exit fullscreen mode

可区分联合体的第一部分是icon定义属性的时候。在这种情况下,我们希望该src属性无效,即不能被设置。

{   
 icon?: never;
 src?: string;
};
Enter fullscreen mode Exit fullscreen mode

第二部分是当iconprop 未指定时(undefined)。这时我们可以毫无问题地传递 src props。

type ConditionalProps =
  | {
      icon?: JSX.Element;
      src?: never;
    }
  | {
      icon?: never;
      src?: string;
    };
Enter fullscreen mode Exit fullscreen mode

现在回到整个区分联合体icon。它表示and props的配置src可以是第一种情况,也可以是第二种情况。

值得注意的是,我们在这个例子中使用了关键字 ` never`。关于这个关键字的最佳解释可以在 TypeScript 文档中找到:

“TypeScript 将使用 never 类型来表示不应该存在的状态。”

To reiterate, we defined two types for two scenarios and combined them using the union operator.

type Props = CommonProps & ConditionalProps  
Enter fullscreen mode Exit fullscreen mode

Props成为交集CommonPropsConditionalProps

Props是两种类型的组合。因此,它将具有所有属性CommonProps,以及我们用创建的这种依赖关系ConditionalProps

最后,在Avatar组件中,`and`icon和 ` srcprops` 都将分别是它们的类型JSX.Element | undefinedstring | undefined因此它们的类型会直接显示出来,就像你没有创建依赖关系一样。

现在如果我们尝试同时提供这两个属性,就会看到一个 TypeScript 错误:

错误图像

条件属性变化

我需要创建一个具有不同变体的组件,每个变体都有一组属性。

我们希望只有在选择了匹配的变体时才提供这些属性。

在我们的例子中,我们有3个变体。"text" | "number" | "element"

  • 如果我们选择将值设置varianttext,则需要一个message类型为的属性string,并且我们不能设置componentName属性。
  • 如果我们选择将值设置variantnumber,则需要有一个message类型为的 props number,并且不能设置componentNameprop。
  • 如果我们传递variantas element,这里我们componentName也可以使用 finally, messageprop 也将变为类型JSX.Element

我们来看一个例子

interface CommonProps {
  children?: React.ReactNode;
  // ...other props that always exist
}
type ConditionalProps =
  | {
      componentName?: string;
      message?: JSX.Element;
      variant?: "element";
    }
  | {
      componentName?: never;
      message?: string;
      variant?: "text";
    }
  | {
      componentName?: never;
      message?: number;
      variant?: "number";
    };

type Props = CommonProps & ConditionalProps;

export const VariantComponent = (props: Props): JSX.Element => {
  const { message, componentName, variant = "element", children } = props;
  return (
    <div>
      {message && message}
      {variant === "element" && componentName}
      {children}
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode
/* 
 * If the we chose to set the variant to text,
 * we need to have a message props of type string,
 * We can't set componentName prop
 */

{
 componentName?: never;
 message?: string;
 variant?: "text";
}
Enter fullscreen mode Exit fullscreen mode
/*
 * If the we chose to set the variant to number,
 * we need to have a message props of type number,
 * and we can't set componentName prop
 */
{
 componentName?: never;
 message?: number;
 variant?: "number";
}
Enter fullscreen mode Exit fullscreen mode
/*
 * If we do pass the variant as element, 
 * here we can use finally componentName
 * also the message prop will become of type JSX.Element
 */
{
 componentName: string;
 message?: JSX.Element;
 variant?: "element";
}
Enter fullscreen mode Exit fullscreen mode

设置variantprop 后,TypeScript 会将组件的类型缩小到其各自所需的属性,并告知您需要提供哪些信息。

具有泛型类型的集合的条件属性

接下来,我们来尝试为 Select 组件定义条件属性。我们的组件需要足够灵活,能够接受字符串数组或对象数组作为其options属性值。

如果组件接收一个对象数组,我们希望开发者指定应该使用这些对象的哪些字段作为标签和值。

集合属性的条件类型

type SelectProps<T> =
  | {
      options: Array<string>;
      labelProp?: never;
      valueProp?: never;
    }
  | {
      options: Array<T>;
      labelProp: keyof T;
      valueProp: keyof T;
    };

export const Select = <T extends unknown>(props: SelectProps<T>) => {
  return <div>{JSON.stringify(props)}</div>;
};

Enter fullscreen mode Exit fullscreen mode

为了匹配用户提供给下拉列表的对象,我们可以使用TypeScript 中的泛型。

{
 options: Array<T>;
 labelProp: keyof T;
 valueProp: keyof T;
}
Enter fullscreen mode Exit fullscreen mode

在第二种类型中,我们将通用对象的optionsprop 从Array<Object>`to`更改Array<T>为 `is`。客户端必须提供一个包含通用对象类型项的数组。

我们使用keyof关键字来告诉 TypeScript,我们期望的labelPropvalueProp通用对象字段。

现在,当您尝试提供valueProp或时labelProp,您将看到基于选项项字段的自动完成建议。

但是,为了避免某些问题,我们必须做一个小小的改动。我们希望确保我们得到的通用对象是一个自定义对象,而不是一个基本类型,例如字符串:

type SelectProps<T> = T extends string
  ? {
      options: Array<string>;
      labelProp?: never;
      valueProp?: never;
    }
  : {
      options: Array<T>;
      labelProp: keyof T;
      valueProp: keyof T;
    };

export const Select = <T extends unknown>(props: SelectProps<T>) => {
  return <div>{JSON.stringify(props)}</div>;
};
Enter fullscreen mode Exit fullscreen mode

在这里,我们使用三元运算符更改联合类型,以检查我们的泛型类型是否为字符串,并根据该类型将组件的类型设置为适当的选项。

这是本教程的代码沙箱链接。

请我喝杯咖啡

文章来源:https://dev.to/elmay/conditional-react-props-with-typescript-43lg