创建类型安全的多态反应组件

问题描述 投票:0回答:1

我们正在尝试为一堆多态反应设计系统组件带来更多的类型安全性(例如,仅允许在

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>>'.

游乐场链接

有人能帮助我理解我在这里做错了什么吗?我觉得这应该是可能的,只是我错过了一些东西。 🤔

reactjs typescript polymorphism
1个回答
0
投票

解决方案

您可以通过将类型参数

AsDomTags
传递给
Body
组件来修复类型错误。

function TestWrappedComponent<As extends AsDomTags = "div">(
  { as, somethingElse, ...props }: TestWrappedComponentProps<As>
) {
  return <Body<AsDomTags> as={as || 'div'} {...props} />;
}

问题本质上是子组件被推断为 `Body,并且 TypeScript 无法检查传递下来的 props 是否有效。这部分是由于代码中不合理的假设,部分是由于类型检查器在解析涉及类型参数的复杂表达式方面的限制。

同样,您可以删除类型参数并将 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} />;
}
© www.soinside.com 2019 - 2024. All rights reserved.