我们正在尝试为一堆多态反应设计系统组件带来更多的类型安全性(例如,仅允许在
htmlFor
标签上使用 label
属性)。
下面是一个人为的示例,其中我们有一个基本多态组件,并且我们希望使用稍微不同的默认
as
属性从该组件进行扩展。
import React, {
ComponentPropsWithoutRef,
ReactNode,
} from "react";
import { jsx as createComponent } from "@emotion/react";
type AsDomTags = "span" | "div" | 'label';
type StandardComponentWithProps<
As extends AsDomTags = "span",
ExtraProps extends {} = {}
> = ComponentPropsWithoutRef<As> & { as?: As } & ExtraProps;
type BodyProps<As extends AsDomTags = "span"> =
StandardComponentWithProps<
As,
{
children: ReactNode;
}
>;
const Body = <As extends AsDomTags = "span">({
as,
children,
...domAttributes
}: BodyProps<As>) => createComponent(
as || "span",
{
...domAttributes,
},
children
);
// WRAPPED COMPONENT (extends from <Body /> component): ---------------------------------------------------
type TestWrappedComponentProps<As extends AsDomTags = "div"> = BodyProps<As> & { somethingElse?: boolean };
function TestWrappedComponent<As extends AsDomTags = "div">(
{ as, somethingElse, ...props }: TestWrappedComponentProps<As>
) {
return <Body as={as || 'div'} {...props} />;
}
function App() {
return (
<TestWrappedComponent as="label" htmlFor="moo" somethingElse>moo</TestWrappedComponent>
);
}
给出 TSC 错误:
Type '{ as: "div" | As; } & Omit<TestWrappedComponentProps<As>, "as" | "somethingElse">' is not assignable to type 'IntrinsicAttributes & BodyProps<"div" | As>'.
Type '{ as: "div" | As; } & Omit<TestWrappedComponentProps<As>, "as" | "somethingElse">' is not assignable to type 'IntrinsicAttributes & PropsWithoutRef<ComponentProps<As>> & { as?: "div" | As | undefined; } & { children: ReactNode; }'.
Type '{ as: "div" | As; } & Omit<TestWrappedComponentProps<As>, "as" | "somethingElse">' is not assignable to type 'PropsWithoutRef<ComponentProps<As>>'.
有人能帮助我理解我在这里做错了什么吗?我觉得这应该是可能的,只是我错过了一些东西。 🤔
您可以通过将类型参数
AsDomTags
传递给 Body
组件来修复类型错误。
function TestWrappedComponent<As extends AsDomTags = "div">(
{ as, somethingElse, ...props }: TestWrappedComponentProps<As>
) {
return <Body<AsDomTags> as={as || 'div'} {...props} />;
}
问题本质上是子组件被推断为 `Body
同样,您可以删除类型参数并将 props 的类型扩展为
AsDomTags
。
function TestWrappedComponent(
{ as, somethingElse, ...props }: TestWrappedComponentProps<AsDomTags>
) {
return <Body as={as||"div"} {...props} />;
}
通过删除类型参数,您可能看起来在类型特异性方面丢失了一些东西,但这两个版本是等效的。由于我将在下面解释的原因,
Body
和TestWrappedComponent
之间类型参数的默认值的差异实际上并不重要。
首先,分配的有效性取决于对默认值的错误假设。类型参数
<As extends AsDomTags = "div">
的默认值与向下传递给 Body
中 as={as || div}
组件的“div”默认值之间没有必然联系。
当没有为
As="div"
属性传递任何值时,代码似乎假设 as
。但是,由于 as
属性可为空,因此可以为 As
提供具体类型,但仍省略 as
。在这种情况下,“div”的默认值将无效,例如如果我们将组件实例化为 TestWrappedComponentProps<"span">
,则 prop 的类型为 as?: "span"
,因此省略 as
仍然是正确的,但将“div”指定为默认值是不正确的。
如果将默认值移至解构表达式,您可以更直接地看到这一点。
function TestWrappedComponent<As extends AsDomTags = "div">(
{ as="div", somethingElse, ...props }: TestWrappedComponentProps<As>
) {
return <Body as={as} {...props} />;
}
您将从函数签名中得到此错误:
Type '"div"' is not assignable to type 'As'.
'"div"' is assignable to the constraint of type 'As', but 'As' could be instantiated with a different subtype of constraint 'AsDomTags'.
换句话说,
As
可能已被实例化为“span”,因此使用“div”作为默认值不一定安全。
让我们看一下您发布的错误的最后两行。
Type '{ as: "div" | As; } & Omit<TestWrappedComponentProps<As>, "as" | "somethingElse">' is not assignable to type 'IntrinsicAttributes & PropsWithoutRef<ComponentProps<As>> & { as?: "div" | As | undefined; } & { children: ReactNode; }'.
Type '{ as: "div" | As; } & Omit<TestWrappedComponentProps<As>, "as" | "somethingElse">' is not assignable to type 'PropsWithoutRef<ComponentProps<As>>'
第一行表示某种类型不可分配给特定的交叉点类型。下一行放大了不满足的交集的特定条件。错误的结构是这样的:
Type 'T' is not assignable to type 'A & B & C'.
(because)
Type 'T' is not assignable to type 'B'.
为了可分配给交集类型
A & B & C
,T
必须可单独分配给 A
、B
和 C
中的每一个。
对我来说有趣的是,类型检查器将
BodyProps<As | "div">
的那部分解析为 'PropsWithoutRef<ComponentProps<As>>'
而不是 'PropsWithoutRef<ComponentProps<As | "div">>'
。看起来可能它正在将 As | "div"
折叠为 As
,因为 "div"
是 As
的可能情况。如果是这样,我不能 100% 确定类型检查器这样做是否正确,但 BodyProps<"div">
不能分配给 BodyProps<"As">
是有道理的,因为 As
可能是 "span"
或 "label"
.
类型检查器并不总是能够解析涉及类型参数的复杂表达式,即使它们直观上是等效的。
如果我修改
StandardComponentWithProps
类型声明,使 as
属性不再是可选的,并删除默认值,我仍然会收到错误:
// this causes a type error
function TestWrappedComponent<As extends AsDomTags = "div">(
{ somethingElse, ...props }: TestWrappedComponentProps<As>
) {
return <Body<As> {...props} />;
}
我仍然收到错误:
Type 'Omit<TestWrappedComponentProps<As>, "somethingElse">' is not assignable to type 'PropsWithoutRef<ComponentProps<As>>'.
但是,如果我从解构表达式中删除
somethingElse
,以便 props
的类型不包含在 Omit<>
中,它就可以正常工作。
// this works
function TestWrappedComponent<As extends AsDomTags = "div">(
{ ...props }: TestWrappedComponentProps<As>
) {
return <Body<As> {...props} />;
}