TypeScript:如何将类型信息从父级传递给子级?
鉴于以下Form及其(不受控制的)Input子项:
<Form initialValues={{ firstName: "", lastName: "", age: "" }}>
<Input label="First name" name="firstName" />
<Input label="Last name" name="lastName" />
<Input label="Age" name="age" />
</Form>
我希望Input's nameprop 是 type "firstName" | "lastName" | "age"。这种类型应该派生自Form's initialValues。
实现这一目标的最干净的方法是什么?
注意:在一般情况下,表单输入组件应该是 tree-shakeable。
回答
这条信息无法自动推断。您必须以一种或另一种方式手动提供类型。临时解决方案如下:
function App() {
const initialValues = {
firstName: 'string',
lastName: 'string',
age: 'string',
}
const MyInput: React.FC<{ label: string, name: keyof typeof initialValues }> = Input
return <Form initialValues={initialValues}>
<MyInput label="First name" name="firstNameWRONG" /> // error
<MyInput label="Last name" name="lastName" />
<MyInput label="Age" name="age" />
</Form>
}
我想谈谈为什么这是不可能的。
1.组件声明自己的协议
我猜您的思维模型是将子组件视为父组件的“参数”,因此从父组件到子组件提出某种“要求”是合理的。
我不会说这样的观点是完全错误的,实际上父子组件可以以耦合方式编写,但它在反应中并不惯用。
理想情况下,反应组件应该通过props type. 您可能将组件视为“服务”,服务消费者有责任遵守协议,而不是相反。
2. TS 类型系统的限制
转译为 JS,这样的结构变成:
function App() {
const initialValues = {
firstName: 'string',
lastName: 'string',
age: 'string',
}
const MyInput: React.FC<{ label: string, name: keyof typeof initialValues }> = Input
return <Form initialValues={initialValues}>
<MyInput label="First name" name="firstNameWRONG" /> // error
<MyInput label="Last name" name="lastName" />
<MyInput label="Age" name="age" />
</Form>
}
首先,如果要引发任何类型错误,则应按行引发parentElement,而不是childElement. 违反的是父组件的协议,该协议规定“子组件name应该是keyof initValue”。而这种验证是由React.createElement函数根据其参数完成的。
其次,从类型系统的角度来看,如果我们能够从 推断出childProps的类型parentProps,那么解析过程应该是这样的:
1. let `Parent` be generic type of form
Component<T, E<_>> = (props: { initValues: T, children: E<keyof T> }) => Element<any>
2. let `Child` be generic type of form
Component<K> = (props: { name: K }) => Element<K>
3. from `childElement = React.createElement(Child, childProps))`
we know `childElement` is type `Element<string>`
4. from `parentElement = React.createElement(Parent, parentProps, childElement)`
we know about `T` and `E = Element` and `_ = keyof T`
5. now we need to allow prioritze `E<_>` rule over `Element<string>`, thus override `childElement` from `Element<string>` to `Element<keyof T>`
为了让这样的类型系统工作,我们既需要支持更高级的类型参数E<_>,也需要某种类型操作的优先级。
实际上,我们需要指定childElement尚未解决,但仍处于挂起状态的Element<_>,然后让下一个解决步骤填写_部分。
另外,我们的意思不是这样的,
Element<T>.fill(arg: T)
但我们的意思是,
fill<T>(arg0: T, arg1: Element<T>)
AFAIK,从来没有一个类型系统支持这种行为,更不用说 TS 甚至不支持更高级的类型。
----
更新
我认为值得一提的是,理论上可以从Formabout namepropInput不遵守协议引发类型错误。但是它不能用 JSX 来完成,只能通过React.createElement.
这是因为 TS 为所有 JSX 创建的元素分配了特殊的内置接口JSX.Element。并且 react 已经将它扩展到了扩展React.ReactElement<any, any>。这any件事有效地最大化了props所有元素的协议,使任何限制成为不可能。
我试图找到解决方法,但不幸的是没有找到。你能得到的最好的是评论中提供的链接中的建议。