score:3

Accepted answer

it is not possible. there is no way to know statically what props usercomponent receives from parent xcomponent in your reactdom.render context.

if you want a type safe solution, use children as functions:

here is xcomponent definition

interface xcomponentprops {
    oncancel: (index: number) => void;
    childrenasfunction: (props: { oncancel: (index: number) => void }) => jsx.element;
}

class xcomponent extends react.component<xcomponentprops> {
    render() {
        const { oncancel } = this.props;
        return <div>{childrenasfunction({ oncancel })}</div>;
    }
}

now you can use it to render your usercomponents

<xcomponent oncancel={handlecancel} childrenasfunction={props => 
  <span>
    <usercomponent {...props} />
    <usercomponent {...props} />
  </span>
} />

this will work nicely, i use this pattern often with no hassle. you can refactor xcomponentprops to type the childrenasfunction prop with the relevant part (the oncancel function here).

score:-1

update: solution 2021

ran into this problem and have since figured out a neat workaround:

gonna use a tabs component as an example but it's the same problem being solved (i.e. keeping ts happy when dynamically adding props to child components, from the parent).


components/tabs/tab.tsx

// props to be passed in via instantiation in jsx
export interface tabexternalprops {
  heading: string;
}

// props to be passed in via react.children.map in parent <tabs /> component
interface tabinternalprops extends tabexternalprops {
  index: number;
  isactive: boolean;
  setactiveindex(index: number): void;
}

export const tab: react.fc<tabinternalprops> = ({
  index,
  isactive,
  setactiveindex,
  heading,
  children
}) => {
  const classname = `tab ${isactive && 'tab--active'}`;
  return (
    <div onclick={() => setactiveindex(index)} {...classname}>
      <strong>{heading}</strong>
      {isactive && children}
    </div>
  )
}

components/tabs/tabs.tsx

import { tab, tabexternalprops } from './tab'; 

interface tabsprops = {
  defaultindex?: number;
}
interface tabscomposition = {
  tab: react.fc<tabexternalprops>;
}

const tabs: react.fc<tabsprops> & tabscomposition = ({ children, defaultindex = 0 }) => {
  const [activeindex, setactiveindex] = usestate<number>(defaultactiveindex);
  const childrenwithprops = react.children.map(children, (child, index) => {
    if (!react.isvalidelement(child)) return child;
    const jsxprops: tabexternalprops = child.props;
    const isactive = index === activeindex;
    return (
      <tab
        heading={jsxprops.heading}
        index={index}
        isactive={isactive}
        setactiveindex={setactiveindex}
      >
        {react.cloneelement(child)}
      </tab>
    )
  })
  return <div classname='tabs'>{childrenwithprops}</div>
}

tabs.tab = ({ children }) => <>{children}</>

export { tabs }

app.tsx

import { tabs } from './components/tabs/tabs';

const app: react.fc = () => {
  return(
    <tabs defaultindex={1}>
      <tabs.tab heading="tab one">
        <p>tab one content here...</p>
      </tab>
      <tabs.tab heading="tab two">
        <p>tab two content here...</p>
      </tab>
    </tabs>
  )
}

export default app;

score:0

you can use interface inheritance (see extending interfaces) and have usercomponentprops extend xcomponentprops:

interface usercomponentprops extends xcomponentprops { caption: string }

this will give usercomponentprops all the properties of xcomponentprops in addition to its own properties.

if you don't want to require usercomponentprops to have all the properties of xcomponentprops defined, you can also use partial types (see mapped types):

interface usercomponentprops extends partial<xcomponentprops> { caption: string }

this will give usercomponentprops all the properties of xcomponentprops, but makes them optional.

score:0

you can pass children as a function, even in jsx. this way you will get proper typing all the way. usercomponents props interface should extend childprops.

interface childprops {
    oncancel: (index: number) => void;
}

interface xcomponentprops {
    oncancel: (index: number) => void;
    children: (props: childprops) => react.reactelement<any>;
}

class xcomponent extends react.component<xcomponentprops> {
    render() {
        const { oncancel } = this.props;
        return children({ oncancel })};
    }
}


<xcomponent oncancel={handlecancel}>
    { props => <usercomponent {...props} /> } 
</xcomponent>

score:0

i know this has been solved, but another solution is to restrict the type of children that can be passed. limits the functionality of the wrapper, but could be what you want.

so if you kept your original example:

interface xcomponentprops {
   oncancel: (index: number) => void;
   children: reactelement | reactelement[]
}

class xcomponent extends react.component<xcomponentprops> {
    render() {
        const { oncancel } = this.props;
        const children = !array.isarray(this.props.children) ?
            react.cloneelement(this.props.children, { oncancel: () => oncancel() }) :
            this.props.children.map((child, idx) => (
                react.cloneelement(child, { oncancel: () => oncancel(idx) })
            ));
        return <div>{children}</div>;
    }
}

interface usercomponentprops { caption: string }
const usercomponent: react.fc = (props: usercomponentprops) => (
    <button onclick={props.onclose}>{props.caption || "close"}</button>
);

reactdom.render(
    <xcomponent oncancel={handlecancel}>
        <usercomponent />
        <usercomponent />
        <usercomponent />
    </xcomponent>
);

Related Query

More Query from same tag