DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for React Pro Tip #2 β€” How to Type `this.props` to Include `defaultProps`
Deckstar
Deckstar

Posted on

React Pro Tip #2 β€” How to Type `this.props` to Include `defaultProps`

This trick is for those occasions when you use a class component, and don't want TypeScript to complain about optional props being possibly undefined, despite definitely being included in defaultProps.

In a word, how to fix this:

TypeScript might not know the that defaultProps were included
Unfortunately, TypeScript doesn't know that defaultProps get added to this.props. And so we get Object is possibly 'undefined'. in places where we shouldn't. Quelle horreur!


Contents:


TLDR

There are two ways to solve this. Both should be simple & easy to understand.


Case 1: Simple

If your Props interface is not used anywhere else, you may want to just remove the optional ?: from the type:

interface RatingProps {
  /**
   * The rating out of 10.
   *
   * @default 10
   */
  rating: number; // <--- remove the `?`
}
Enter fullscreen mode Exit fullscreen mode

As long as the prop is included in the component's defaultProps, React's typing should be smart enough to not force you to specify the prop when using the component:

return <Rating /> // `rating` is required, but we don't need to specify it because it's included in `defaultProps`
Enter fullscreen mode Exit fullscreen mode

This is the recommended way to solve this problem, if you're writing the code from scratch. This solves the "possibly undefined" error within the component without creating errors elsewhere, provided that no other types depend on the Props interface.

But if interface Props {} is already interdependent with other types, and you don't want to refactor them, then you might benefit from the fancier solution below:


Case 2: Slightly more complicated

It may be you can't make the props required, or feel it would be unnecessarily complicated to do so (for example, if many other types depend on the type of your props). In that case, you can still fix this error.

All you need is this one-line trick:

import React, { Component } from "react";

interface RatingProps {
  /**
   * The rating out of 10.
   *
   * @default 10
   */
  rating?: number;
}

class Rating extends Component<RatingProps> {
// ---------
  declare readonly props: RatingProps &
    Required<Pick<RatingProps, keyof typeof Rating.defaultProps>>; // <--- Add me!
// ---------

  constructor(props) {
    super(props);
  }

  static defaultProps = {
    rating: 10,
  };

  render() {
    const { rating } = this.props;

    if (rating < 5) return <p>Fail</p>;
    return <p>{rating}</p>;
  }
}

export default Rating;
Enter fullscreen mode Exit fullscreen mode

(Well, two if you include formatting... πŸ˜‰)

This method should be fool-proof: this.props should now be perfectly typed within the component, and no other side-effects should be produced in any types anywhere else.

It is very easy to make TypeScript be able to understand that defaultProps were included in this.props
Declare your own props, and voilΓ ! Problem solved!


Explained

React's type definitions file (index.d.ts) defines the props as:

readonly props: Readonly<P>;
Enter fullscreen mode Exit fullscreen mode

The reason method #1 works, is because of this definition in the same file:

type ReactManagedAttributes<C, P> = C extends { propTypes: infer T; defaultProps: infer D; }
    ? Defaultize<MergePropTypes<P, PropTypes.InferProps<T>>, D>
    : C extends { propTypes: infer T; }
        ? MergePropTypes<P, PropTypes.InferProps<T>>
        : C extends { defaultProps: infer D; }
            ? Defaultize<P, D>
            : P;
Enter fullscreen mode Exit fullscreen mode

in which React infers the leftover required props based on the supplied defaultProps.

Method #2 works because of the following: by declaring our own props at the top of the component, we simply overwrite React's definition of props inside the component definition itself to not only use the P (i.e., the type of the component's passed in Props) but also its defaultProps. (In TypeScript, this is known as a "type-only field declaration".)

Pretty neat, huh?


Another pro-tip: I've actually written a reusable helper type β€” WithDefaultProps β€” to make this trick easier:

/**
 * Like `Required`, but you can choose which keys to make required. (`Required` makes all keys required).
 *
 * ---
 *
 * Example:
 * `typescript
 * type Obj = { a?: 1; b?: 2; c?: 3 };
 * type PartiallyRequiredObj = Imposed<Obj, 'a' | 'b'>; // equivalent to `{ a: 1; b: 2; c?: 3 }`;
 * `
 */
export type Imposed<T, K extends keyof T> = T & Required<Pick<T, K>>;

/**
 * A helper for React class components with `static defaultProps`.
 *
 * To use, simply add `declare readonly props:` at the top of the class:
 * `ts
 * class MyComponent extends Component<Props>{
 *  declare readonly props: WithDefaultProps<Props, typeof MyComponent.defaultProps>;
 *
 *  // ...
 * }
 * `
 */
export type WithDefaultProps<PassedInProps, DefaultProps extends object> =
  Readonly<Imposed<PassedInProps, keyof DefaultProps>>;

Enter fullscreen mode Exit fullscreen mode

Drawbacks

As far as I can tell, there are no drawbacks to either of these methods. (Keep in mind that method #1 is just using React's defaultProps in a way that's working as intended).

For the custom props declaration method, try to keep in mind that you are forcefully overwriting the props. You must beware of typos which might ruin your type, especially when copy-pasting. Forgetting about this fact might lead to surprises if you don't keep in mind where the types come from.

Also, note that you have to know that defaultProps will definitely be passed in, because TypeScript won't know this. And a React novice might not know that either, so it might be a good reason to add a JSDoc comment over your custom props declaration. (Not to mention that the seniors will probably wonder what the heck you're doing πŸ˜„)

Otherwise, use it to your heart's content! πŸ™‚


Conclusion

In this article, we saw two quick & easy ways to make sure that your this.props always remains type-safe.

If you're one of the few people still using class components, then this tip may have been helpful for you πŸ˜„


Further reading:

Top comments (0)

Dream Big


Use any Linode offering to create something unique or silly in the DEV x Linode Hackathon 2022 and win the Wacky Wildcard category.

β†’ Join the Hackathon <-