React / tsx:子级无法从父级推断正确的泛型类型

时间:2019-07-17 16:57:09

标签: reactjs typescript

我想获取一些能够正确确定我的类型何时不兼容的代码,并使用children而不是prop。这是正确错误的基准:

type Option<T> = {
  value: T;
  text: string;
}

type SelectProps<T> = {
  value: T;
  options: Option<T>[];
}
const options = [
  { value: 5, text: 'Five' },
  { value: 10, text: 'Ten'}
];
return <Select value="Text" options={options} />; // ERROR: value isn't type number

但是当我使用孩子时,我似乎并没有出错:

type OptionProps<T> = {
  value: T;
  children: string;
}

type SelectProps<T> {
  value: T;
  children: React.ReactElement<OptionProps<T>>;
}
/* No errors here */

<Select value="Text">
  <Option value={5}>Five</Option>
  <Option value={10}>Ten</Option>
</Select>

我在此codeandbox中放了一个更完整的示例(可以从下面找到沙箱中的代码):https://codesandbox.io/s/throbbing-sun-tg48b-注意renderA如何正确识别错误,其中renderB错误地没有错误。

import * as React from 'react';

type OptionA<T> = {
  value: T;
  text: string;
}

type SelectAProps<T> = {
  value: T;
  options: OptionA<T>[];
  onClick: (value: T) => void;
}

class SelectA<T> extends React.Component<SelectAProps<T>> {
  renderOption = (option: OptionA<T>) => {
    const { value, text } = option;
    const onClick = () => {
      this.props.onClick(value)
    };
    return <div onClick={onClick}>{text}</div>
  }

  render(): React.ReactNode {
    return <div>{this.props.options.map(this.renderOption)}</div>
  }
}

type OptionBProps<T> = {
  value: T;
  children: string;
}

class OptionB<T> extends React.Component<OptionBProps<T>> {}

type SelectBProps<T> = {
  value: T;
  children: React.ReactElement<OptionBProps<T>>[];
  onClick: (value: T) => void;
}

class SelectB<T> extends React.Component<SelectBProps<T>> {
  renderOption = (option: OptionB<T>) => {
    const { value, children } = option.props;
    const onClick = () => {
      this.props.onClick(value)
    };
    return <div onClick={onClick}>{children}</div>
  }

  render(): React.ReactNode {
    return <div>{React.Children.map(this.props.children, this.renderOption)}</div>
  }
}

class Main extends React.Component {
  onClick(value: string) {
    console.log(value);
  }

  renderA(): React.ReactNode {
    const options = [
      { value: 5, text: 'Five' },
      { value: 10, text: 'Ten'}
    ]
    return <SelectA value="Text" options={options} onClick={this.onClick} />
  }

  renderB(): React.ReactNode {
    return (
      <SelectB value="Text" onClick={this.onClick}>
        <OptionB value={5}>Five</OptionB>
        <OptionB value={10}>Ten</OptionB>
      </SelectB>
    );
  }
}

2 个答案:

答案 0 :(得分:3)

不幸的是,到目前为止,您无法像示例中所示对children道具实施类型约束。以前,我实际上认为这种类型检查 是可能的。很好奇,我研究了一下:

TypeScript将通过默认的JSX factory function将您的JSX代码转换为原始的React.createElement语句。 React.createElement的返回类型是JSX.Element(全局JSX命名空间),它扩展了React.ReactElement<any>

这意味着,当渲染SelectB<T>时,您的OptionB组件将表示为JSX.Element[]React.ReactElement<any>[]

给出道具类型

type SelectBProps<T> = {
  value: T;
  children: React.ReactElement<OptionBProps<T>>[];
  onClick: (value: T) => void;
}

,TypeScript可以将ReactElement<OptionBProps<T>>[]ReactElement<any>[]进行比较,并且可以成功编译(给定T的具体类型)。

What TypeScript docs say

  

默认情况下,JSX表达式的结果键入为any。您可以通过指定JSX.Element接口来自定义类型。但是,无法从此接口检索有关JSX的元素,属性或子级的类型信息。这是一个黑匣子。

您可以证明这种行为(JSX表达式解析为JSX.Element):只需将SelectBProps替换为:

type SelectBProps<T> = {  ... ;  children: number[]; ... };

对于每个OptionB,您现在都会收到错误:

  

类型“元素”不能分配给类型“数字”

本地范围的JSX名称空间(附加信息)

从TypeScript 2.8开始,支持本地范围的JSX名称空间,这可以帮助对JSX.Element使用自定义类型/类型检查。

TypeScript docs on locally scoped JSX namespaces

  

JSX类型检查由JSX命名空间中的定义驱动,例如JSX.Element用于JSX元素的类型,而JSX.IntrinsicElements用于内置元素。在TypeScript 2.8之前,JSX名称空间应位于全局名称空间中,因此只能在项目中定义它。从TypeScript 2.8开始,将在jsxNamespace(例如React)下查看JSX名称空间,从而允许在一个编译中包含多个jsx工厂。

此外,此非常有用的article中的一句话:

  

TypeScript支持本地范围的JSX,从而能够支持各种JSX工厂类型和每个工厂正确的JSX类型检查。尽管当前的反应类型仍使用全局JSX命名空间,但将来会有所变化。

-

最后,声明children类型

  • 出于文档目的
  • 将其标记为强制性(默认为可选属性)
  • 从一个元素数组中区分一个JSX.Element

如果您仍然想进行广泛的类型检查,也许坚持使用props以前的解决方案可能是一个有效的主意?最后,children只是props的另一种形式,并且Render Props已经确立为一个坚实的概念。

虽然不能解决您原来的children类型检查用例,但我希望可以将其清除一些(对我来说,是did)!

欢呼

答案 1 :(得分:0)

我认为向下通过React.Children.map传递类型信息是导致类型推断混乱的原因。如果您在this.props.children中查看React.Children.map的类型,您会发现它具有React.ReactElement<OptionBPropts<T>>类型,但也有许多其他可能的类型,具有any

我会改用渲染道具方法。

import * as React from "react";

type OptionA<T> = {
  value: T;
  text: string;
};

type SelectAProps<T> = {
  value: T;
  options: OptionA<T>[];
  onClick: (value: T) => void;
};

class SelectA<T> extends React.Component<SelectAProps<T>> {
  renderOption = (option: OptionA<T>) => {
    const { value, text } = option;
    const onClick = () => {
      this.props.onClick(value);
    };
    return <div onClick={onClick}>{text}</div>;
  };

  render(): React.ReactNode {
    return <div>{this.props.options.map(this.renderOption)}</div>;
  }
}

type OptionBProps<T> = {
  value: T;
  children: string;
};

type SelectBProps<T> = {
  value: T;
  children: ({
    renderOption
  }: {
    renderOption: (
      option: OptionBProps<T>
    ) => React.ReactElement<OptionBProps<T>>;
  }) => React.ReactElement<OptionBProps<T>>;
  onClick: (value: T) => void;
};

class SelectB<T> extends React.Component<SelectBProps<T>> {
  renderOption = (option: OptionBProps<T>) => {
    const { value, children } = option;
    const onClick = () => {
      this.props.onClick(value);
    };
    return <div onClick={onClick}>{children}</div>;
  };

  render() {
    return this.props.children({ renderOption: this.renderOption });
  }
}

class Main extends React.Component {
  onClick(value: string) {
    console.log(value);
  }

  renderA(): React.ReactNode {
    const options = [{ value: 5, text: "Five" }, { value: 10, text: "Ten" }];
    return <SelectA value="Text" options={options} onClick={this.onClick} />;
  }

  renderB(): React.ReactNode {
    return (
      <SelectB<string> value="Text" onClick={this.onClick}>
        {({ renderOption }) => (
          <>
            {renderOption({ value: 5, children: "Five" })}
            {renderOption({ value: 10, children: "Ten" })}
          </>
        )}
      </SelectB>
    );
  }
}

Edit thirsty-galois-rgcog

https://codesandbox.io/embed/thirsty-galois-rgcog