React 中休息道具的 TypeScript 工作区

为 TypeScript 2.1更新

TypeScript 2.1现在支持对象扩展/rest ,因此不再需要变通方法!


原始问题

TypeScript 支持 JSX 传播属性,它在 React 中通常用于将 HTML 属性从组件传递到呈现的 HTML 元素:

interface LinkProps extends React.HTMLAttributes {
textToDisplay: string;
}


class Link extends React.Component<LinkProps, {}> {
public render():JSX.Element {
return (
<a {...this.props}>{this.props.textToDisplay}</a>
);
}
}


<Link textToDisplay="Search" href="http://google.com" />

然而,React 引入了 如果将任何未知的道具传递给 HTML 元素,则发出警告。上面的示例将产生一个 React 运行时警告,说明 textToDisplay<a>的未知支柱。对于这样的例子,建议的解决方案是使用 对象静止属性对象静止属性提取出您的自定义道具,并使用其余的 JSX 扩展属性:

const {textToDisplay, ...htmlProps} = this.props;
return (
<a {...htmlProps}>{textToDisplay}</a>
);

但是 TypeScript 还不支持这种语法。我知道,希望有一天 我们将能够在 TypeScript 中做到这一点。(< em > 更新: TS 2.1现在支持对象扩展/休息!你怎么还在看这个?与此同时,有什么变通办法吗?我正在寻找一个解决方案,不妥协类型安全,并发现它出奇的困难。例如,我可以这样做:

const customProps = ["textDoDisplay", "otherCustomProp", "etc"];
const htmlProps:HTMLAttributes = Object.assign({}, this.props);
customProps.forEach(prop => delete htmlProps[prop]);

但是,这需要使用字符串属性名,而这些属性名没有针对实际的道具进行验证,因此容易出现输入错误和不良的 IDE 支持。还有更好的办法吗?

98338 次浏览

You probably can't avoid creating a new object with a subset of the properties of this.props, but you can do that with type safety.

For example:

interface LinkProps {
textToDisplay: string;
}


const LinkPropsKeys: LinkProps = { textToDisplay: "" };


class Link extends React.Component<LinkProps & React.HTMLAttributes, {}> {
public render(): JSX.Element {
return (
<a { ...this.getHtmlProps() }>{ this.props.textToDisplay }</a>
);
}


private getHtmlProps(): React.HTMLAttributes {
let htmlProps = {} as React.HTMLAttributes;


for (let key in this.props) {
if (!(LinkPropsKeys as any)[key]) {
htmlProps[key] = this.props[key];
}
}


return htmlProps;
}
}

Using LinkPropsKeys object, which needs to match the LinkProps, will help you keep the keys between the interface and the runtime lookup synchronized.

I've accepted Nitzen Tomer's answer because it was the basic idea I was going for.

As a more generalized solution this is what I ended up going with:

export function rest(object: any, remove: {[key: string]: any}) {
let rest = Object.assign({}, object);
Object.keys(remove).forEach(key => delete rest[key]);
return rest;
}

So I can use it like this:

const {a, b, c} = props;
const htmlProps = rest(props, {a, b, c});

And once TypeScript supports object rest/spread I can just look for all usages of rest() and simplify it to const {a, b, c, ...htmlProps} = props.

React.HtmlAttributes in the example above is now generic so I needed to extend from React.AnchorHTMLAttributes<HTMLAnchorElement>.

Example:

import React from 'react';


type  AClickEvent = React.MouseEvent<HTMLAnchorElement>;


interface LinkPropTypes extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
to: string;
onClick?: (x: AClickEvent) => void;
}


class Link extends React.Component<LinkPropTypes> {
public static defaultProps: LinkPropTypes = {
to: '',
onClick: null,
};


private handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
...
event.preventDefault();
history.push(this.props.to);
};


public render() {
const { to, children, ...props } = this.props;
return (
<a href={to} {...props} onClick={this.handleClick}>
{children}
</a>
);
}
}


export default Link;

A getter like this could work:

class Link extends React.Component<{
textToDisplay: string;
} & React.HTMLAttributes<HTMLDivElement>> {


static propTypes = {
textToDisplay: PropTypes.string;
}


private get HtmlProps(): React.HTMLAttributes<HTMLAnchorElement> {
return Object.fromEntries(
Object.entries(this.props)
.filter(([key]) => !Object.keys(Link.propTypes).includes(key))
);
}


public render():JSX.Element {
return (
<a {...this.HtmlProps}>
{this.props.textToDisplay}
</a>
);
}
}


<Link textToDisplay="Search" href="http://google.com" />

It's actually easier than all of the answers above. You just need to follow the example below:

type Props = {
id: number,
name: string;
// All other props
[x:string]: any;
}


const MyComponent:React.FC<Props> = props => {
// Any property passed to the component will be accessible here
}

Hope this helps.

use ...rest

type ButtonProps = {
disabled: boolean;
};


function Button(props: ButtonProps): JSX.Element {
const {disabled = false, ...rest} = props;
...
return (
<button disabled={disabled} {...rest}>
....

TypeScript now ignores ...rest if you pass it as argument to your component. In my opinion ...rest argument does not need type safety as these are the default argument that are passed down to child components by parent component. For example redux passes information about store to child component and so the ...rest argument is always there and does not needs type safety or propTypes.

//here's the right solution


interface schema{
loading: boolean
}
//pass ...rest as argument
export function MyComponent({loading, ...rest}:schema){
if (loading) return <h2>Loading ...</h2>
return (
<div {...rest}>
<h2>Hello World </h2>
</div>
}

strong text

For those who might not quickly understand what [x:string]: any; in the accepted answer does: Although it's a lot like arrays' syntax, it's indeed specifying an object its keys which are of type string and its values which are of type any. It's called "Index Signature" in TypeScript's terminology.

However, also notice that sometimes, as an alternative and less loose-on-types solution, a library you're using might have types exported as well, so you could use those.

For instance, when extending Ant's Buttons, one could do this:

import { ReactNode } from "react";
import { Button as AntButton } from "antd";
import { NativeButtonProps } from "antd/lib/button/button";


interface IButtonProps {
children?: ReactNode;
}


const Button = ({
children,
...rest
}: IButtonProps & NativeButtonProps): JSX.Element => {
return <AntButton {...rest}>{children}</AntButton>;
};


export default Button;

NOTE1: The ampersand (&) operator in IButtonProps & NativeButtonProps simply does "merging" of types in TypeScript. Now you don't lose intellisense for Ant Button props on your own Button, because you don't use any anymore. Ant Button's types and your IButtonProps are combined and so exist both.

NOTE2: Also you might wonder where I found this type. This type was exported here: https://github.com/ant-design/ant-design/blob/master/components/button/button.tsx#L124 And also its include path could be realized using intellisense, just start typing NativeButton... and it must be suggested to you.

React.ComponentPropsWithoutRef/React.ComponentPropsWithRef

As explained in https://react-typescript-cheatsheet.netlify.app/docs/advanced/patterns_by_usecase/

interface Props extends React.ComponentPropsWithoutRef<"button"> {
// ...
}
    

const FancyButton = (props: Props) => {
const { /**/ , ...rest} = props
      

// ...
      

return <button {...rest}>{/**/}</button>
}

if using forwardRef, use React.ComponentPropsWithRef instead

DEMO