我想创建一个通用接口来包含一个组件和它的属性值。
想法是能够为同一道具设置接受不同类型的不同组件。
场景是这样的:
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>
);
}
你试图完成的事情不能以目前的形式(或可能永远)完成。这是 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 编译器需要
- 意识到它需要同时缩小b
- 取出函数中所有以前的流程控制语句涉及 a 或 b
- 通过所有这些语句同时详尽地分析 a 和 b 的并集可能性的乘积
RyanCavanaugh(TypeScript 开发负责人)的评论提出了一个很好的建议:
如前所述,这将非常复杂,而且可以通过一两次检查耗尽的类型域相对较少。以对编译器更友好的方式构建此代码也会使其更人性化,所以我认为如果这不起作用也不算太糟糕。
不是在返回值中评估 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>
);
}