This content originally appeared on DEV Community 👩💻👨💻 and was authored by Deckstar
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:
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
- Explained
- Drawbacks
- Conclusion
- Further reading
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 `?`
}
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`
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;
(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.
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>;
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;
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>>;
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:
- React's type definitions file
- TypeScript's "type-only" field declarations
This content originally appeared on DEV Community 👩💻👨💻 and was authored by Deckstar
Deckstar | Sciencx (2022-10-19T18:11:46+00:00) React Pro Tip #2 — How to Type `this.props` to Include `defaultProps`. Retrieved from https://www.scien.cx/2022/10/19/react-pro-tip-2-how-to-type-this-props-to-include-defaultprops/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.