119

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?

4
  • Note, that syntax you're looking for is now available Commented May 23, 2017 at 18:16
  • @KyleGobel True, with much rejoicing on my part. :) Should this question just be deleted? Commented May 23, 2017 at 18:26
  • 2
    "TypeScript 2.1 now supports object spread/rest, so no workarounds are needed anymore!" HOW do you use this?!! Commented Apr 27, 2019 at 8:17
  • @gyozokudor Just use TypeScript 2.1 or greater (it's up to 3.4 now!) and use the original example: const {textToDisplay, ...htmlProps} = this.props Commented Apr 29, 2019 at 13:05

10 Answers 10

208

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
}
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks, but I think you misunderstood the error. It's not a TypeScript compile error, it's a React runtime error caused by passing (via JSX spread syntax) properties to an HTML element that are not recognized, like textToDisplay on an <a> tag in the above example. In any rate, this has been long solved by TS 2.1 adding support for object rest/spread syntax.
Huh. My original question must have been poorly worded, because this answer does not address it at all yet has the most upvotes. :-P I'm guessing a lot of people are googling "rest props" trying to figure out how to overload a type with arbitrary props, and indeed the permissive index type here does that. My question was about ES6 spread/rest syntax and type safety.
Even though this answer doesn't address the actual question, I want to mention that you can implement the component like: export const Icon = ({className = '', icon, ...props}: Props) => { return <Whatever icon={icon} className={className} {...props}/>; }; to avoid using props like this: props.className //or props.icon
Beware: this is still a hack. It might cause you to lose track of missing crucial props. Don't make it your thing.
27

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.

Comments

16

use ...rest

type ButtonProps = {
    disabled: boolean;
};

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

3 Comments

but how do you actually infer that whats being passed to button is actually a valid HTML prop or event?
Tried this but get a TS error where I'm passing in additional props, but adding [x:string]: any; to the type as in @Willy's answer above fixed that.
In this case you should use interface with inheritance, like this: interface ButtonProps extends JSX.Element { disabled: boolean; } But, @Metalik's solution is more simple and works greate.
15

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.

2 Comments

Better answer here ^
This is good but what if I'm passing the props to the component from another file? Let's say that <Button> is in another file, how do I infer the props there where I actually define the <Button> component?
13

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;

Comments

6

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

Comments

4

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

Comments

2

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.

Comments

0

If you are looking for code intelligence

use the following approach.

  1. pass the ...rest props.
  2. choose which element in the component to have intelligence (TextInput) in our case.
  3. Extend the component props by the element type.

AppTextInput.tsx

interface AppTextInputProps extends TextInputProps {
icon?: any;
}
const AppTextInput = ({ icon, ...rest }: AppTextInputProps) => {
  return (
    <View style={styles.container}>
     <TextInput style={styles.input} placeholder="Name" {...rest} />
      {icon && <Ionicons name={icon} size={16} />}
    </View>
    );
 };

Comments

-1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.