React 组件的通用接口和具有类型推断的属性值

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

我想创建一个通用接口来包含一个组件和它的属性值。

想法是能够为同一道具设置接受不同类型的不同组件。

场景是这样的:

interface FormComponentProps<T> { initialValueForEdit?: T }

interface FormAFields { valueA: string }

function FormA(props: FormComponentProps<FormAFields>): ReactElement {
  const { initialValueForEdit } = props; // initialValueForEdit has type FormAFields
  ...
}

interface FormBFields { valueB: string }

function FormB(props: FormComponentProps<FormBFields>): ReactElement {
  const { initialValueForEdit } = props; // initialValueForEdit has type FormBFields
  ...
}

我尝试了以下

FormStateInterface
但是在尝试渲染它时出现了问题。

formState.valueForEdit
类型不是从
formState.FormComponent
推断出来的,然后它在将
initialValueForEdit
属性设置为
formState.valueForEdit
时给出以下错误:

Type 'FormAFields' is not assignable to type 'FormAFields & FormBFields'.

我们如何让 TS 推断出

FormComponent
道具和
valueForEdit
被限制为同一类型?

interface FormStateInterface<T> {
  valueForEdit?: T;
  FormComponent: (props: FormComponentProps<T>) => ReactElement;
}

type AvailableFormStates =
  FormStateInterface<FormAFields> | FormStateInterface<FormBFields>;

function App(): ReactElement {
  const [formState, setFormState] = useState<AvailableFormStates | null>(null);

  // No problem here, valueForEdit type is infered from FormA component props
  const formAValue: FormAFields = { valueA: 'foo' };
  const showFormA = () => setFormState({ FormComponent: FormA, valueForEdit: formAValue });

  // Same as above
  const formBValue: FormBFields = { valueB: 'bar' };
  const showFormB = () => setFormState({ FormComponent: FormB, valueForEdit: formBValue });

  return (
    // Error on initialValueForEdit prop
    // Type 'FormAFields' is not assignable to type 'FormAFields & FormBFields'.
    <div>
      {formState ? <formState.FormComponent initialValueForEdit={formState.valueForEdit} /> : (
        <div>
          <button onClick={showFormA}>Show Form A</button>

          <button onClick={showFormB}>Show Form B</button>
        </div>
      )};
    </div>
  );
}
reactjs typescript typescript-generics type-inference
1个回答
0
投票

你试图完成的事情不能以目前的形式(或可能永远)完成。这是 Typescript 被有意设计为处理模糊函数的结果。查看说明或跳至可能的解决方案。

说明

观察下面的思路

type Foo = {foo: 'foo'}
type Bar = {bar: 'bar'}
const useFoo = (n: Foo) => null!
const useBar = (s: Bar) => null!
type NumberOrStringInterface = {
  fn: typeof useFoo
} | {
  fn: typeof useBar
}
const someObject: NumberOrStringInterface = {} as any
someObject.fn({foo: 'foo'})
// Argument of type '{ foo: "foo"; }' is not assignable to parameter of type 'Foo & Bar'.
  Property 'bar' is missing in type '{ foo: "foo"; }' but required in type 'Bar'.

someObject.fn({bar: 'bar'})
// Argument of type '{ bar: "bar"; }' is not assignable to parameter of type 'Foo & Bar'.
  Property 'foo' is missing in type '{ bar: "bar"; }' but required in type 'Foo'.

someObject.fn({foo: 'foo', bar: 'bar'})
// No errors

这仅仅是因为,Typescript 不知道函数中需要哪个对象,因为它不知道在运行时实际要调用哪个函数。因此它将与它们相交,如果您提供

foo
bar
它仍然适用于
useFoo
useBar
.

自然地,在您的代码中,

formState.valueForEdit
将始终与相同的功能组件对齐。但是类型系统怎么知道呢?简短的回答,它不会,而且可能永远不会。对于这种逻辑上的缩小来说,代价太大了。关于将连续推导类型的类型系统记录了这样一个问题:逻辑连续类型保护·问题#37258·microsoft/TypeScript,这也有助于证明问题的另一个例子:

function isNull(a: string | null): a is null {
  return a == null;
}

function doStuff(a: string | null, b: string | null) {
  if (a == null && b == null) {
    return;
  }

  if (a == null && b != null) {
    return;
  }

  if (a != null && b == null) {
    return;
  }

  a; // logically must be `string`, but inferred as `string | null`
  b; // logically must be `string`, but inferred as `string | null`
  a.length; // Object is possibly 'null'
}

让编译器推理这类事情听起来真的很酷,直到你意识到实现起来有多慢。即使在这个玩具箱中,为了缩小 a 编译器需要

  1. 意识到它需要同时缩小b
  2. 取出函数中所有以前的流程控制语句涉及 a 或 b
  3. 通过所有这些语句同时详尽地分析 a 和 b 的并集可能性的乘积

-nmain 2020 年 3 月 9 日

RyanCavanaugh(TypeScript 开发负责人)的评论提出了一个很好的建议:

如前所述,这将非常复杂,而且可以通过一两次检查耗尽的类型域相对较少。以对编译器更友好的方式构建此代码也会使其更人性化,所以我认为如果这不起作用也不算太糟糕。

-RyanCavanaugh 2020 年 3 月 12 日

解决方案

不是在返回值中评估 FormA 或 FormB,而是提前评估 JSX 并存储它。

function App1(): React.ReactElement {
  // JSX.Element and ReactElement are the same type
  const [formState, setFormState] = useState<JSX.Element | null>(null);

  // I have provided this in the case where you need some shared value 
  // between the two different components
  const sharedValue = { sharedValue: true }

  const formAValue: FormAFields = { valueA: 'foo', ...sharedValue };
  const showFormA = () => setFormState(<FormA initialValueForEdit={formAValue} />);

  const formBValue: FormBFields = { valueB: 'bar', ...sharedValue };
  const showFormB = () => setFormState(<FormB initialValueForEdit={formBValue} />);

  return (
    <div>
      {formState ?? (
        <div>
          <button onClick={showFormA}>Show Form A</button>

          <button onClick={showFormB}>Show Form B</button>
        </div>
      )};
    </div>
  );
}

在 Typescript Playground 上查看示例

© www.soinside.com 2019 - 2024. All rights reserved.