使用 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>
);
};
如果我们在导入组件时同时传递这两个属性,该组件就不会引发任何错误。
因此,我们必须向开发者提供提示,告诉他们同时传递这两个参数是被禁止的,只需抛出一个 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>
);
};
对于已经熟悉 TypeScript 的人来说,这些信息应该足够了。
然而,短短几行代码却包含了很多功能。如果你想了解这些代码的含义和工作原理,让我们把它分解成几个部分来讲解。
interface CommonProps {
children: React.ReactNode
// ...other props that always exist
}
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;
};
ConditionalProps这就是奇迹发生的地方。它被称为“区别性联合”。它是对象定义的联合。
让我们进一步分析,然后再来看看这个受歧视的工会是如何为我们服务的。
{
icon?: JSX.Element;
src?: never;
}
可区分联合体的第一部分是icon定义属性的时候。在这种情况下,我们希望该src属性无效,即不能被设置。
{
icon?: never;
src?: string;
};
第二部分是当iconprop 未指定时(undefined)。这时我们可以毫无问题地传递 src props。
type ConditionalProps =
| {
icon?: JSX.Element;
src?: never;
}
| {
icon?: never;
src?: string;
};
现在回到整个区分联合体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
Props成为和的交集。CommonPropsConditionalProps
Props是两种类型的组合。因此,它将具有所有属性CommonProps,以及我们用创建的这种依赖关系ConditionalProps。
最后,在Avatar组件中,`and`icon和 ` srcprops` 都将分别是它们的类型JSX.Element | undefined,string | undefined因此它们的类型会直接显示出来,就像你没有创建依赖关系一样。
现在如果我们尝试同时提供这两个属性,就会看到一个 TypeScript 错误:
条件属性变化
我需要创建一个具有不同变体的组件,每个变体都有一组属性。
我们希望只有在选择了匹配的变体时才提供这些属性。
在我们的例子中,我们有3个变体。"text" | "number" | "element"
- 如果我们选择将值设置
variant为text,则需要一个message类型为的属性string,并且我们不能设置componentName属性。 - 如果我们选择将值设置
variant为number,则需要有一个message类型为的 propsnumber,并且不能设置componentNameprop。 - 如果我们传递
variantaselement,这里我们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>
);
};
/*
* 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";
}
/*
* 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";
}
/*
* 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";
}
设置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>;
};
为了匹配用户提供给下拉列表的对象,我们可以使用TypeScript 中的泛型。
{
options: Array<T>;
labelProp: keyof T;
valueProp: keyof T;
}
在第二种类型中,我们将通用对象的optionsprop 从Array<Object>`to`更改Array<T>为 `is`。客户端必须提供一个包含通用对象类型项的数组。
我们使用keyof关键字来告诉 TypeScript,我们期望的labelProp是valueProp通用对象字段。
现在,当您尝试提供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>;
};
在这里,我们使用三元运算符更改联合类型,以检查我们的泛型类型是否为字符串,并根据该类型将组件的类型设置为适当的选项。

