How to type any param with React Typescript in Functional Component

Viewed 1877

I have an existing javascript component:

const RenderState = ({ state, else: elseChild = undefined, ...states }) => {
  if (state && states[state]) return states[state]
  if (elseChild) return elseChild
  return null
}

export default RenderState

I am trying to convert it to typscript and have so far:

import React from 'react'

interface Props {
  state: string
  else?: JSX.Element
  any: JSX.Element
}

const RenderState: React.FC<Props> = ({ state, else: elseChild = undefined, ...states }) => {
  if (state && states[state]) return states[state]
  if (elseChild) return elseChild
  return null
}

export default RenderState

The problem I am having is of course that any: JSX.Element is looking for a prop literally called 'any' when instead I want to allow any prop as a JSX element, such as custom in the following example

Here is an example use case of this component:

import React from 'react'
import RenderState from './RenderState'

const MyComp = () => {
  const [state, setState] = React.useState<string | null>(null)
  <>
    <Button onClick={()=>{setState(null)}}>Default</Button>
    <Button onClick={()=>{setState('custom')}}>Custom</Button>
    <RenderState
      state={state}
      custom={(<p>Some custom content here...</p>)}
      else={(<p>Some default content here...</p>)}
    />
  </>
}
1 Answers

Sonds like you need index signature on your type. A naive approach would be

interface Props {
  state: string
  else?: JSX.Element
  [key: string]: JSX.Element
}

However, this won't compile - because index signature on an interface requires that all named properties (state and else) are compatible with it.

Instead you can rewrite it in a form of type intersection:

type Props = {
  state: string
  else?: JSX.Element
} & Record<string, JSX.Element>

This still won't compile for the same, but gives us a clue. What if instead of string for keys we have a well-known set of possible values? Next iteration would be

type Key = "custom" | "another";

type Props = {
  state: Key | null
  else?: JSX.Element
} & Record<Key, JSX.Element>

This looks better. But how do we make it reusable, so that the same component could be used with different lists of possible Keys? Let's make it generic!

So the final code:

type Props<Key extends string> = {
  state: Key | null
  else?: JSX.Element
} & Record<Key, JSX.Element>

You need to make the component a generic function as well:

function RenderState<Key>(props: Props<Key>) { ... }

And use it in your app:

    const [state, setState] = React.useState<"custom" | null>(null);
    <RenderState
      state={state}
      custom={(<p>Some custom content here...</p>)}
      else={(<p>Some default content here...</p>)}
    />

Edit 1

This approach allows to write props[props.state] inside RenderState (the result is inferred to JSX.Element). However, it fails to compile with rest destructuring, as in the original code snippet:

const {state, else: elseEl, ...rest} = props;
rest[props] // TS error here

It reveals a flaw in our type definition. We need to make sure state value doesn't include "state" and "else" literals, to avoid type conflict.

Updated type would be


type Props<Key extends string> = {
  state: Exclude<Key, "state" | "else"> | null
  else?: JSX.Element
} & Record<Key, JSX.Element>

Now this should compile well

const {state, else: elseEl, ...rest} = props;
rest[props]
Related