I have an existing javascript ponent:
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 ponent:
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>)}
/>
</>
}
I have an existing javascript ponent:
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 ponent:
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>)}
/>
</>
}
Share
Improve this question
asked Nov 25, 2020 at 18:19
amasteramaster
2,1635 gold badges28 silver badges55 bronze badges
1
- Those seem like bad prop names to me. Best not to use existing JS/TS keywords as your own... – paddotk Commented Jun 10, 2021 at 8:55
1 Answer
Reset to default 7 +50Sonds 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 pile - because index signature on an interface requires that all named properties (state
and else
) are patible 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 pile 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 ponent 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 ponent 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 pile 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 pile well
const {state, else: elseEl, ...rest} = props;
rest[props]