I'm relatively new to typescript, although having a long experience with react (and prop-types).
I've encountered a problem with typing my component, if there's another component as a prop. I already have a Button
component with a defined type
.
Now the problem is, when I create a new component ButtonGroup
having a children
prop that only should allow Button
components to be rendered. I'm comparing the child
's props to the Button
type props - this means that I can pass there another component like Input
if I don't break a button prop - like having <Input id="input-id" />
is fine for typescript, but I don't want to allow that, because it's a different type of component.
I've successfully played with the component.type.displayName
, but Typescript doesn't like this solution at all.
import { ReactElement, Children } from 'react'
type buttonVariantType = 'primary' | 'secondary' | undefined
type buttonProps = {
id?: string
variant?: buttonVariantType
children: ReactNode
...
}
const Button =({ children, id, variant = 'secondary' }) => {
...
return <button ...>{children}</button>
}
interface buttonGroupProps {
children: ReactElement<buttonProps>[]
variant?: buttonVariantType
}
const ButtonGroup = ({ children, variant }: buttonGroupProps) => {
return (
<div>
{Children.map(children, (child) => {
// TS2339: Property 'displayName' does not exist on type 'string | JSXElementConstructor<any>'
if (child?.type.displayName !== 'Button') {
console.log(child.type.displayName, 'not a button')
throw Error('not a button') // ugly workaround, still unacceptable by typescript
}
return (
<Button {...child?.props} variant={variant ?? child.props.variant}>
{child.props.children}
</Button>
)
})}
</div>
)
}
Now if I put another component as a children
into ButtonGroup, the typescript is okay with that and I don't want that.
<ButtonGroup variant="danger">
<Input id="123" />
<Button variant="primary" id="primary">
Primary button
</Button>
<Button variant="secondary" id="secondary">
Secondary button
</Button>
</ButtonGroup>
It's not possible to make it compile time safe,reason for that is, when you declare any component with the jsx tags, the type that's created through that contains any
for the values in the type. Let me give you an example -
import * as React from "react";
type buttonProps = {
id?: string;
variant?: 'primary' | 'secondary';
children?: React.ReactNode;
}
const Button = ({ children, id, variant = 'secondary' }: buttonProps) => {
return <button>{children}</button>
}
const div = () => <div></div>;
const input = () => <input></input>;
const Btn = () => <Button></Button>;
type Button = React.ReactElement<buttonProps, React.JSXElementConstructor<buttonProps>>;
type Div = ReturnType<typeof div>;
type Input = ReturnType<typeof input>;
type Btn = ReturnType<typeof Btn>;
type ExpandedButton = { [Property in keyof Button]: Button[Property] }
// ^? type ExpandedButton = { type: React.JSXElementConstructor<buttonProps>; props: buttonProps; key: React.Key | null; }
type ExpandedDiv = { [Property in keyof Div]: Div[Property] };
// ^? type ExpandedDiv = { type: any; props: any; key: React.Key | null; }
type ExpanedInput = { [Property in keyof Input]: Input[Property] };
// ^? type ExpanedInput = { type: any; props: any; key: React.Key | null;
type ExpandedBtn = { [Property in keyof Btn]: Btn[Property] };
// ^? type ExpanedBtn = { type: any; props: any; key: React.Key | null;
type Assignable = (ExpandedButton extends (ExpandedDiv | ExpanedInput | ExpandedBtn | Div | Input | Btn) ? true : false) | ((ExpandedDiv | ExpanedInput | ExpandedBtn | Div | Input | Btn) extends ExpandedButton ? true : false);
// ^? type Assignable = true
Which goes to show, any jsx element can be assigned to the other. As for a runtime error, you got it nearly right, the check that you should be making is to see if the component that's the child is actually the Button
component through reference equality check, like this -
import { ReactElement, Children } from 'react'
type buttonVariantType = 'primary' | 'secondary' | undefined
type buttonProps = {
id?: string
variant?: buttonVariantType
children?: React.ReactNode
}
const Button =({ children, id, variant = 'secondary' }: buttonProps) => {
return <button>{children}</button>
}
interface buttonGroupProps {
children: ReactElement<buttonProps>[]
variant?: buttonVariantType
}
const ButtonGroup = ({ children, variant }: buttonGroupProps) => {
return (
<div>
{Children.map(children, (child) => {
if (child?.type !== Button ) {
throw Error('not a button')
}
return (
<Button {...child?.props} variant={variant ?? child.props.variant}>
{child.props.children}
</Button>
)
})}
</div>
)
}