Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript: How to pass type information from parent to child?

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.

like image 745
Misha Moroshko Avatar asked Mar 01 '23 15:03

Misha Moroshko


1 Answers

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.

1. Component declares its own protocol

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.

2. Limitation in TS type system

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.

—-

Update

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.

like image 189
hackape Avatar answered Mar 04 '23 03:03

hackape