TypeScript workaround for rest props in React

ReactjsTypescript

Reactjs Problem Overview


Updated for TypeScript 2.1

TypeScript 2.1 now supports object spread/rest, so no workarounds are needed anymore!


Original Question

TypeScript supports JSX spread attributes which is commonly used in React to pass HTML attributes from a component to a rendered HTML element:

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" />

However, React introduced a warning if you pass any unknown props to an HTML element. The above example would produce a React runtime warning that textToDisplay is an unknown prop of <a>. The suggested solution for a case like this example is to use object rest properties to extract out your custom props and use the rest for the JSX spread attributes:

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

But TypeScript does not yet support this syntax. I know that hopefully some day we will be able to do this in TypeScript. (Update: TS 2.1 now supports object spread/rest! Why are you still reading this??) In the meantime what are some workarounds? I'm looking for a solution that doesn't compromise type-safety and finding it surprisingly difficult. For example I could do this:

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

But this requires the use of string property names that are not validated against the actual props and thus prone to typos and bad IDE support. Is there a better way we can do this?

Reactjs Solutions


Solution 1 - Reactjs

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.

Solution 2 - Reactjs

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.

Solution 3 - Reactjs

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;

Solution 4 - Reactjs

use ...rest

type ButtonProps = {
    disabled: boolean;
};

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

Solution 5 - Reactjs

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" />

Solution 6 - Reactjs

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.

Solution 7 - Reactjs

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAaron BeallView Question on Stackoverflow
Solution 1 - ReactjsWillyView Answer on Stackoverflow
Solution 2 - ReactjsNitzan TomerView Answer on Stackoverflow
Solution 3 - ReactjsDamian GreenView Answer on Stackoverflow
Solution 4 - ReactjsMetalikView Answer on Stackoverflow
Solution 5 - ReactjsOkkuView Answer on Stackoverflow
Solution 6 - ReactjsAaron BeallView Answer on Stackoverflow
Solution 7 - ReactjsWaleed TariqView Answer on Stackoverflow