DEV Community

A. Sharif
A. Sharif

Posted on

Notes on TypeScript: Inferring React PropTypes

Introduction

These notes should help in better understanding TypeScript and might be helpful when needing to lookup up how to leverage TypeScript in a specific situation. All examples are based on TypeScript 3.2.

PropTypes and Inference

More often than not we might be working on an existing React application, where a team has decided to introduce TypeScript. This would also mostly mean that if Components props have to be defined at some point, either defining all at once or gradually. In some cases there also might be existing prop-type definitions. Instead of removing the existing prop-type definitions, we might be able to build a bridge between these prop-types and TypeScript.

In this part of the "Notes on TypeScript" we will learn how we can leverage TypeScript to infer these component prop-types.

Before we begin, it's important to note that the PropTypes type package offers PropTypes.InferProps, which enables to infer the types for an existing prop-type definition like so:

const userPropTypes = {
  id: PropTypes.number.isRequired,
  name: PropTypes.string.isRequired,
  active: PropTypes.bool
};

types UserProps = PropTypes.InferProps<typeof userPropTypes>;
Enter fullscreen mode Exit fullscreen mode

But let's take a step back for a minute. What does calling typeof on a PropType definition return?

type TypeOfUserProps = typeof userPropTypes;

/*
  type TypeOfUserProps = {
    id: PropTypes.Validator<number>;
    name: PropTypes.Validator<string>;
    active: PropTypes.Requireable<boolean>;
  }
*/
Enter fullscreen mode Exit fullscreen mode

If we take a closer look at userPropTypes, we can see that id and name are required and that the active flag is optional. When inferring these prop-type definitions, we can see that the returned type definition wraps the defined types into either a Validator or Requireable, depending on the fact, that the type has been defined as required or not.

So internally PropTypes.InferProps differentiates between required and optional types and then creates an intersection between these two groups. We won't go too deep into the implementation details, but there is a need to find out if a prop-type is required or not. An internal IsOptional type checks if a type is null | undefined and then determines if the type is optional.

Next, let's build a small example, to verify if we can transform any prop-type definitions to actual TypeScript types. We have a User component with existing prop-type and default props definitions.

const userPropTypes = {
  id: PropTypes.number.isRequired,
  name: PropTypes.string.isRequired,
  active: PropTypes.bool
};

const userDefaultProps = {
  name: "Test"
};

const User = (props /*: PropTypes? */) => {
  return (
    <div>
      id: {props.id}
      name: {props.name}
      status: {props.active ? "active" : "inactive"}
    </div>
  );
};

User.defaultProps = userDefaultProps;
Enter fullscreen mode Exit fullscreen mode

How can we infer these userPropTypes and provide the missing types for the User component?

Our first approach would be to go back to the very first example in this write-up:

const userPropTypes = {
  id: PropTypes.number.isRequired,
  name: PropTypes.string.isRequired,
  active: PropTypes.bool
};

types UserProps = PropTypes.InferProps<typeof userPropTypes>;
Enter fullscreen mode Exit fullscreen mode

If we verify the example, we can see that this is already working as expected.

const User = (props: UserProps) => {
  // ...
}

<User id={1} /> // Works!
<User id="1!" /> // Error! Type 'string' is not assignable to type 'number'
<User /> // Error! Property 'id' is missing
Enter fullscreen mode Exit fullscreen mode

But we're not considering any default props, although this should not be the case, they might have a different type. This means we need to extend the existing mechanism to include the inference of default props.
There is not very much we need to do:

type InferPropTypes<
  PropTypes,
  DefaultProps = {},
  Props = PropTypes.InferProps<PropTypes>
> = {
  [Key in keyof Props]: Key extends keyof DefaultProps
    ? Props[Key] | DefaultProps[Key]
    : Props[Key]
};
Enter fullscreen mode Exit fullscreen mode

If we take a closer look at the above InferPropTypes type, we can notice that we accept the prop-type and default prop types and then infer the provided prop-types. Next, we map over these inferred prop types and check if a key is also defined in the default props. The key extends keyof DefaultProps part helps us to find out if a key actually exists in the default props. If this is the case we return a union of the prop and default prop value type else we only return the prop value type.

Finally we can use our newly defined InferPropTypes as shown in the next example.

type UserProps = InferPropTypes<typeof userPropTypes, typeof userDefaultProps>;
Enter fullscreen mode Exit fullscreen mode

Running our User component again, shows that everything works as expected.

const User = (props: UserProps) => {
  // ...
}

<User id={1} /> // Works!
<User id="1!" /> // Error! Type 'string' is not assignable to type 'number'
<User /> // Error! Property 'id' is missing
Enter fullscreen mode Exit fullscreen mode

We should have a basic understanding of how we can infer existing prop-type definition in a React application when working with TypeScript.

If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif

Top comments (24)

Collapse
 
eriksjaastad profile image
Erik Sjaastad

Hey :)
I can shrink this down but I already had this up. codesandbox.io/s/proptypes-ts-debu...

This is using the destructured props, I did try not making it required and using propName! when using it. That seems to work but it seems like TS is still missing the part about the default being set

Collapse
 
busypeoples profile image
A. Sharif

Excellent, thanks! Will have a look at this and try to figure out how we can solve this.

Collapse
 
eriksjaastad profile image
Erik Sjaastad

It has been really confusing and since most of the components are written this way, I run into the same issue over and over. Thank god you took the time to write everything else! I come back to Notes on TypeScript over and over again <3

Thread Thread
 
busypeoples profile image
A. Sharif

If you add the defaultProps definition to the Text component, it should probably be enough for TypeScript to figure out that the property is optional.

Text.defaultProps = {
  size: 5
};
Thread Thread
 
eriksjaastad profile image
Erik Sjaastad

That should be the same as adding the value to the destructured props. I updated Text.tsx, the component is being implemented in App.tsx and the same error comes up Property 'size' is missing in type '{ children: string; }' but required in type

Thread Thread
 
busypeoples profile image
A. Sharif

This is interesting. Can you take a look at the simplified example:
codesandbox.io/s/fancy-bird-vqd9x

If you remove defaultProps TypeScript will complain.

Thread Thread
 
busypeoples profile image
A. Sharif

I think the problem in this specific case is tied to using forwardRef. Will try to find out in more detail , but I think forwardRef drops any default props.

Thread Thread
 
eriksjaastad profile image
Erik Sjaastad

Hrm. Removing isRequired and the defaultProps removes the TS error also. Even adding in React.FC<Props> it still works without isRequired. I think I've narrowed it down to incorporating forwardRef, as soon as that is added it seems to lose it's connection to the given prop value and default props codesandbox.io/s/nostalgic-pascal-...

Thread Thread
 
busypeoples profile image
A. Sharif

Yes, I think the problem is within forwardRef in this case. It drops the defaultProps.

Thread Thread
 
eriksjaastad profile image
Erik Sjaastad

You have no idea how long I've been ignoring narrowing down THAT problem! I've been looking at everything else except that. And finding that out, just lead me to this :D github.com/facebook/flow/issues/74...
Thank you soooo much! You've been a life saver!

Thread Thread
 
eriksjaastad profile image
Erik Sjaastad

It turns out that forwardRef was just another one of the issues. If you add a method to the name prop like name.toUpperCase() TS will argue that the value can be null even when you give it the default value and it doesn't complain when implementing <DisplayName />. This describes the issue in stateless functions with defaultProps. It doesn't look like this has really been solved. github.com/microsoft/TypeScript/is...
Hotell's solution does work thought codesandbox.io/s/fast-pond-lhno0

Thread Thread
 
busypeoples profile image
A. Sharif

Thanks for the clarification!

Collapse
 
pacahon profile image
Sergei

Thanks for the article. I've tried this solution, but stuck with an error:

Type 'string | null | undefined' is not assignable to type 'string | undefined'

<div className={className}
                  ~~~~~~~~~

  node_modules/@types/react/index.d.ts:1645:9
    1645         className?: string;

I'm using React class component declaration and @types/react

const defaultProps = {
    className: 'user'
};
const propTypes = {
    className: PropTypes.string,
};

class User extends React.Component<UserProps> {
    static defaultProps = defaultProps;
    static propTypes: {};

    render() {
        let {className} = this.props;
        return (
            <div className={className}>test</div>
        );
    }
}
User.propTypes = propTypes;

Without declaring className on propTypes it fails with an error Property 'className' does not exist on type'Readonly<InferPropTypes<{...`

What could be the reason for this?

Collapse
 
busypeoples profile image
A. Sharif

Thanks for the question. Could you build a codesandbox example, so we can see the complete example and then try to fix it from there?

Collapse
 
pacahon profile image
Sergei

Sure, here it is codesandbox.io/s/weathered-snowfla...
Looks like If disable strict mode in tsconfig.json it works fine.

Thread Thread
 
busypeoples profile image
A. Sharif

Excellent, thanks!
Will try it out and provide some feedback.

Thread Thread
 
eriksjaastad profile image
Erik Sjaastad

Was this solved? This is one of the few articles using InferProps and thank you so much for writing it!

Collapse
 
noriste profile image
Stefano Magni

Thank you for your amazing article!! 👏
One question: how do you manage callback props? The solution above does not help with them because the proptypes does not allow to manage them effectively. For example, I could have this ComponentProps type in my component

export interface ComponentProps {
  handleCheckboxChange: (id: string, checked: boolean) => void;
}

how could I update the InferPropTypes type to be prompted by TS if I pass a callback with a wrong signature?
Thank you so much

Collapse
 
busypeoples profile image
A. Sharif

Thanks for the kind words!
I will have a closer look tomorrow and try to provide more insights if possible.

Collapse
 
adham_benhawy profile image
Adham El Banhawy

That's really interesting. I didn't know you can still use propTypes with TypeScript, I just always assumed that you gotta always write interfaces for props.

Collapse
 
dmorrison profile image
Derek Morrison

I'm confused. In the first example, the (optional) active prop is of type PropTypes.Requireable<boolean>.

Doesn't that mean it's required? What does "requireable" mean?

Collapse
 
dmorrison profile image
Derek Morrison

Ohhhhh - nevermind. That Requireable interface has a method on it called isRequired. So, it's for things can possibly can be made required. 👍

Collapse
 
squidsoup profile image
Kit Randel

Thanks for the excellent post. How do you manage typing spread props?

Collapse
 
busypeoples profile image
A. Sharif

Sorry, haven't been checking any comments for some time now. Will look into this and add more information.