Given the following Form
and its (uncontrolled) Input
children:
<Form initialValues={{ firstName: "", lastName: "", age: "" }}>
<Input label="First name" name="firstName" />
<Input label="Last name" name="lastName" />
<Input label="Age" name="age" />
</Form>
I'd like the Input
's name
prop to be of type "firstName" | "lastName" | "age"
. This type should be derived from the Form
's initialValues
.
What's the cleanest way to achieve this?
Note: In the general case, form input components should be tree-shakeable.
This piece of information cannot be automatically inferred. You'll have to manually provide the type one way or another. And ad-hoc solution would be like:
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>
}
I'd like to talk about why it's impossible.
I guess your mind model is to see child component as a "argument" to parent component, so it's reasonable to pose some kind of "requirement" from parent to child.
I wouldn't say such point of view is totally wrong, cus in practice parent-child component could be written in a coupling fashion, but it's not idiomatic in react.
Ideally, a react component should announce its own "protocol" through props type
. You may think of a component as a "service", it's the responsibility of the service consumer to comply with the protocol, not the other way around.
Transpiled to JS, such structure becomes:
parentElement = React.createElement(Parent, parentProps,
(childElement = React.createElement(Child, childProps))
)
First, if any type error were to be raised, it should be raise by the parentElement
line, not childElement
. What is violated is the protocol of parent component, which states "child component's name
should be keyof initValue
". And such verification is done by React.createElement
function against its argument.
Second, from type system point of view, if we were able to infer childProps
's type from parentProps
, then the resolution process should goes like:
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>`
For such type system to work, we need to both support higher-kinded type parameter E<_>
and also some sort of precedence of type operation.
Effectively we need to specify that childElement
must not be resolved yet, but remain at a pending state of Element<_>
, and then let the next resolution step to fill-in _
part.
Plus, we don't mean anything like,
Element<T>.fill(arg: T)
But we mean,
fill<T>(arg0: T, arg1: Element<T>)
AFAIK, there's never a type system support such behavior, not to mention that TS doesn't even support higher-kinded type to begin with.
—-
I think it’s worth mentioning that, it’s theoretically possible to raise type error from Form
about name
prop of Input
not complying with protocol. However it cannot be done with JSX, only possible through React.createElement
.
This is due to TS assigns all JSX created elements the special builtin interface JSX.Element
. And react has augmented it to extend React.ReactElement<any, any>
. The any
thing effectively max out the props
protocol of all elements, making any restriction impossible.
I tried to find workaround but unfortunately nothing found. The best you can get is what suggested in the link provided in comment.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With