首页 > 解决方案 > 使用联合类型泛型时推断单个类型(扩展反应组件道具)

问题描述

我正在尝试定义一个接受许多预定义属性的包装 React 组件。属性之一 ( tag) 的存在和值还应确定是否有其他属性可用。react 组件将充当所提供tag组件的包装器,该组件将使用包装组件未专门处理的任何道具进行渲染。如果tag省略,则包装器仍然可以接受未处理的道具并将其转发给子组件,但不提供任何类型(简单Record<string, any>或类似的就可以了)。

不幸的是,我还没有找到一种方法来阻止 Typescript 在tag省略道具时生成所有可能属性的联合。

Codesandbox上提供了该问题的最小复制。我试图解决的问题在其中得到了进一步的解释,但是TL;DR:

<Field tag="textarea" name="test" cols={10} /> // cols is correctly typed
<Field tag="input" name="test" cols={10} /> // cols is correctly marked as invalid
<Field name="omitted" cols={10} /> // cols is typed here even though it's not a prop on <input>

代码摘录:

import * as React from "react";
import * as ReactDOM from "react-dom";

export type FieldTagType = React.ComponentType | "input" | "select" | "textarea";

type TagProps<Tag> = Tag extends undefined ? {}
  : Tag extends React.ComponentType<infer P> ? P
  : Tag extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[Tag]
  : never;

export type FieldProps<Tag extends FieldTagType> = {
  /** Field name */
  name: string;
  /** Element/component to createElement(), defaults to 'input' */
  tag?: Tag;
};

function Field<Tag extends FieldTagType>({
  name,
  tag,
  ...props
}: FieldProps<Tag> & TagProps<Tag>) {
  return React.createElement(tag || "input", {
    name,
    id: name + "-id",
    placeholder: name,
    ...props
  } as any);
}

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    {/*
      <textarea> supports the `cols` attribute, as expected.
    */}
    <Field tag="textarea" name="textarea" cols={10} />
    {/*
      <input> doesn't support the `cols` attribute, and
      since `tag` is properly set typescript will warn about it.
    */}
    <Field tag="input" name="test" cols={10} />
    {/*
      When `tag` is omitted it appears that FieldTagType
      is expanded into every possible type, rather than falling
      back to not providing any additional properties.
      This results in `cols` being included in the list of defined props,
      even though it is not supported by the `input` being rendered as fallback.

      Is it possible to modify the typings of TagProps/FieldProps/Field
      so that omitting `tag` will not include any additional properties,
      but also not error?
      Eg. TagProps are expanded to `Record<string, any>`
    */}
    <Field name="omitted" cols={10} />
  </React.StrictMode>,
  rootElement
);

标签: reactjstypescripttypescript-generics

解决方案


当您在其参数中Field没有tag属性的情况下调用时,这不会导致编译器推断"input"或什undefined至推断Tag泛型参数。由于tag是可选属性,因此未定义的值tag可以应用于 的任何有效选择Tag。例如,您可以指定Tagas"textarea"并仍然保留该tag属性:

Field<"textarea">({ name: "xyz", cols: 10 }); // no error

当然,这不太可能真正发生。无论如何,当您省略编译器时tag,编译器不知道Tag应该是什么,只是将其默认为它的约束, FieldTagType.

如果您希望它默认为"input",您可以设置一个通用参数 default

function Field<Tag extends FieldTagType = input>({
// default here ----------------------> ~~~~~~~ 
    name,
    tag,
    ...props
}: FieldProps<Tag> & TagProps<Tag>) {
    return React.createElement(tag || "input", {
        name,
        id: name + "-id",
        placeholder: name,
        ...props
    } as any);
}

这不会影响您对其他情况的推断,但是当tag省略时,现在您会得到空TagProps<undefined>的 forprops和所需的错误:

    <Field name="omitted" cols={10} /> // error!
    // -----------------→ ~~~~
    // Property 'cols' does not exist on type
    // 'FieldProps<&quot;input"> & TagProps<&quot;input">'

Playground 代码链接


推荐阅读