英文:
react-typescript - How do I allow a specific component type to pass typescript check?
问题
我相对于TypeScript还比较新,尽管在React(和prop-types)方面有很长时间的经验。
我遇到了一个关于为我的组件定义类型的问题,如果有另一个组件作为属性。我已经有一个已定义type
的Button
组件。
现在的问题是,当我创建一个新的组件ButtonGroup
,它具有一个children
属性,该属性只允许渲染Button
组件。我正在将子组件的属性与Button
类型的属性进行比较 - 这意味着我可以传递另一个组件,如Input
,如果我不破坏按钮属性 - 就像<Input id="input-id" />
对TypeScript来说是可以的,但我不想允许这样做,因为它是一种不同类型的组件。
我已经成功地使用了component.type.displayName
,但TypeScript完全不喜欢这个解决方案。
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') // 丑陋的解决方法,TypeScript仍然不接受
}
return (
<Button {...child?.props} variant={variant ?? child.props.variant}>
{child.props.children}
</Button>
)
})}
</div>
)
}
现在,如果我将另一个组件作为ButtonGroup
的children
放入,TypeScript会接受,我不希望发生这种情况。
<ButtonGroup variant="danger">
<Input id="123" />
<Button variant="primary" id="primary">
Primary button
</Button>
<Button variant="secondary" id="secondary">
Secondary button
</Button>
</ButtonGroup>
英文:
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>
答案1
得分: 0
以下是您要翻译的代码部分:
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 ExpandedDiv = { [Property in keyof Div]: Div[Property] };
type ExpanedInput = { [Property in keyof Input]: Input[Property] };
type ExpandedBtn = { [Property in keyof Btn]: Btn[Property] };
type Assignable = (ExpandedButton extends (ExpandedDiv | ExpanedInput | ExpandedBtn | Div | Input | Btn) ? true : false) | ((ExpandedDiv | ExpanedInput | ExpandedBtn | Div | Input | Btn) extends ExpandedButton ? true : false);
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>
)
}
英文:
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>
)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论