1

The thing I'd like to achieve is to create generic-typed component which would accept different props (based on its usage in project), then finally output its' children prop with all the given props (flow-through) + possibly some new consts (which would be calculated based on the props), like so:

const bonus1 = { offerId: 1, offerDate: 1911 }
const bonus2 = { offerId: 2, timeLeft: 100 }
const bonus3 = { offerId: 3, offerDate: 1991, isActive: true }

<Bonus {...bonus1}>
{({ offerId, offerDate, onClick }) => {})
</Bonus>

<Bonus {...bonus2}>
{({ offerId, timeLeft, onClick }) => {})
</Bonus>

<Bonus {...bonus3}>
{({ offerId, offerDate, isActive, onClick }) => {})
</Bonus>

I've prepared some common type with fields I know every bonus would have, like: offerId:

type BonusCommon = { offerId: number }

Now the component types:

type BonusProps<T> = {
  children: (renderProps: RenderProps<T>) => ReactNode
} & T

type RenderProps<T> = {
  onClick: () => void
} & T // here is what I thought would work, ie. pass to `children` all the input props

And the component itself:

const Bonus = <T extends BonusCommon>({
  children,
  offerId,
  ...rest, // the rest of the input props
}: BonusProps<T>) => {
  const onClick = () => {} // do something with available props

  return (
    <>
      children({ // TS ERROR!
        onClick,
        offerId,
        ...rest
      })
    </>
  )
}

I am out of ideas that would actually make it work, cos currently there's an error saying: 'T' could be instantiated with a different subtype of constraint 'BonusCommon'

Typescript playground

user1346765
  • 172
  • 1
  • 15

2 Answers2

1
export type BonusCommon = {
  offerId: number
}

type RenderChildren<T> = {
  onClick: () => void
} & T

export type BonusProps<T extends BonusCommon> = {
  children: (props: RenderChildren<T>) => any
} & T

export const Bonus = <T extends BonusCommon>({
  children,
  ...rest
}: BonusProps<T>) => {
  const onClick = () => {}

  return children({
    onClick,
    ...(rest as unknown as T),
  })
}
  • offerId need to be part of rest that way you can say it's a type T
  • now { onClick: () => void; } & Omit<BonusProps<T>, "children"> type doesn't match with RenderChildren<T> so, first cast an unknown and then as T
1

It is possible to infer all types you want without type assertion.

In order to do it you just need to curry your component.

import React from 'react'

export type BonusCommon = {
  offerId: number
}

type ClickHandler = {
  onClick: () => void
}

export type BonusProps<T> = T & ClickHandler

type RenderChildren<T> = {
  children: (props: T) => JSX.Element
}

export const withRest = <
  Keys extends PropertyKey,
  Value,
  Rest extends Record<Keys, Value>,
  >(initialProps: Rest) => <
    OfferId extends BonusCommon,
    Children extends RenderChildren<BonusCommon & ClickHandler & Rest>
  >({
    children,
    offerId,
  }: OfferId & Children) => {
    const onClick = () => { }
    return children({ ...initialProps, offerId, onClick })
  }
const Bonus = withRest({ hello: 1 })

const jsx = <Bonus offerId={42}>{
  (elem) => {
    elem.offerId // ok
    elem.hello // ok

    elem.onClick // ok
    return <div></div>
  }

}</Bonus>

Playground

withRest is a function which expects rest props and returns valid React component. Returned component in turn knows about Rest props and able to handle them.

As you might have noticed, children callback argument is aware about all props.

Related answers:First, second and my article

If you want to learn more about argument inference you can read this