Jolly
Jolly

Reputation: 1768

Infer type from parent props

I'd like to know if it's somehow possible, in TypeScript, to infer a type based on the prop of the parent.
Let me explain better with an example: suppose I have <Parent> and <Child> components, and an interface IMyComplexObject.

interface IMyComplexObject {
    name: string;
    surname: string;
}

const MyApp = (props) => {
    const [obj, setObj] = React.useState<IMyComplexObject>({name: 'Foo', surname: 'Bar'});

    return (
        <Parent context={obj}>
            <Child value={'name'} />
        </Parent>
    );
}

So, what I'm asking is: when I write <Child value={'name'} />, is it possible to infer the accepted values for that value prop to be 'name' | 'surname', which are the keys of obj?

I know that, if I had only one component, things would be easy. Suppose this:

interface IChildProps<T> {
    ctx: T;
    value: keyof T;
}

- - -

return <Child ctx={obj} value={'name'} />

Then, the the intellisense would telling me which are the accepted values:

enter image description here

But, things get trickier when it's the <Parent> component that defines the context, and not the component itself.

EDIT: To answer @captain-yossarian proposal, the issue with generic type on <Child> is that I would end up with a code like:

<Parent context={obj}>
    <Child<IMyComplexObject> value={...} />
    <Child<IMyComplexObject> value={...} />
    <Child<IMyComplexObject> value={...} />
    ...
</Parent>

Way too many generics (one for each child). More important, there's NO actual connection between the type of the obj passed to <Parent> and the generic types passed to the <Child>s component.
So, for instance, something like this would still be 'legal':

interface IObj_1 {
    age: number;
}

interface IObj_2 {
    name: string;
}


const [obj1, setObj1] = React.useState<IObj_1>({age: 7});
const [obj2, setObj2] = React.useState<IObj_2>({name: 'Foo'});

return (
    <Parent context={obj1}>
        <Child<IObj_2> value={'name'} />
    </Parent>
);

Upvotes: 2

Views: 2857

Answers (1)

Yes , it is possible.

You need to provide an explicit generic argument to Child component.

<Child<MyComplexObject> value={"name"} >

and Child implementation:

const Child=<T,>(props: {value: keyof T})=>null

Or

const Child=<T,>(props: {ctx: T, value: keyof T})=>null

UPDATE

I think the most simple approach will be to provide Parent with extra props: items and callback

items - array of keys of IMyComplexObject. items itself is a primitive value which goes straight to value property of Child

callback - Array.prototype.map callback.

import React, { useState } from 'react'


interface IMyComplexObject {
  name: string;
  surname: string;
}

type ParentProps<Context> = {
  context: Context;
  items: Array<keyof Context>;
  callback: (prop: keyof Context) => JSX.Element
}

type ChildProps = { value: string }

const Parent = <Context,>({ items, callback }: ParentProps<Context>) =>
  <>
    {items.map(callback)}
  </>

const Child = ({ value }: ChildProps) => <p>{value}</p>

const MyApp = () => {
  const [context, setContext] = useState<IMyComplexObject>({ name: 'Foo', surname: 'Bar' });

  return (
    <>
      <Parent
        context={context}
        items={['name']}
        callback={
          (prop) => <Child value={prop} />
        }
      />

      <Parent
        context={context}
        items={['hello']} /** expected error */
        callback={
          (prop) => <Child value={prop} />
        }
      />
    </>
  );
}

Playground

Upvotes: 1

Related Questions