This is a proposal for new type in @types/react which would allow obtaining the correct, defaultized type of component props for use outside of the component.
@microsoft, @sandersn, @andy-ms, @DovydasNavickas, @Jessidhia, @tkrotoff, @ferdaber, @franklixuefei, @mattmccutchen, @johnnyreilly, @RyanCavanaugh, @hotell, @matt-tingen, @eps1lon
As per https://github.com/microsoft/TypeScript/pull/24422#issuecomment-395168742, Props type parameter of a component should be considered "internal" (what you read inside of the component). Therefore, all props with a default value (defaultProps) should be declared as required.
const defaultProps = {
y: 'Y',
};
type Props = {
x: string;
} & typeof defaultProps;
// Props is {x: string; y: string}
const Component = (props: Props) => (
<>
{/* ✅ both x and y are string; it's safe to call a method on them */}
{props.x.toLowerCase()}
{props.y.toLowerCase()}
</>
);
Component.defaultProps = defaultProps;
Since TS 3.1, y is correctly assumed optional when Component is used:
// ✅ only x is required; y has default value
const c = <Component x="X" />;
So far so good. But what if we need to use the Props type outside of the component?
// 💥 Property 'y' is missing
const componentProps: ComponentProps = {x: 'X'};
const c = <Component {...componentProps} />;
The above ComponentProps type could be either imported (import {Props as ComponentProps} from 'component') or obtained with React.ComponentProps type (type ComponentProps = React.ComponentProps<typeof Component>).
The problem is, as said before, Props is _internal_ type. If used outside of the component, we have to take into account defaultProps and make these props optional.
AFAIK, there isn't any way in @types/react how to get this _"defaultised"_ props type. It's a source of misunderstandings and wild creativity (as in https://github.com/microsoft/TypeScript/issues/30251#issuecomment-471328293) how to correctly type components with default props.
Add new generic type (could be called ComponentDefaultizedProps), which would be similar to React.ComponentProps but it would infer the type of defaultProps and make all props with a default value optional in the result type.
type ComponentProps = React.ComponentDefaultizedProps<typeof Component>;
// ✅ No error, y is optional.
const componentProps: ComponentProps = {x: 'X'};
The current code contains types Defaultize and ReactManagedAttributes which basically allows achieving this, but unfortunately, they are not exported.
I have implemented locally in my project react module augmentation to add this ComponentDefaultizedProps:
import React from 'react';
declare module 'react' {
/**
* Infers type of props of a component and makes all default props optional in the result type.
*
* Inspired by ComponentProps, but doesn't take into account JSX.IntrinsicElements
* https://github.com/DefinitelyTyped/DefinitelyTyped/blob/38be717/types/react/index.d.ts#L765-L770
*/
export type ComponentDefaultizedProps<T extends React.JSXElementConstructor<any>> =
T extends React.JSXElementConstructor<infer P>
? DefaultizedProps<T, P>
: {};
}
// Copy of https://github.com/DefinitelyTyped/DefinitelyTyped/blob/38be717/types/react/index.d.ts#L2787-L2796
type Defaultize<P, D> = P extends any
? string extends keyof P ? P :
& Pick<P, Exclude<keyof P, keyof D>>
& Partial<Pick<P, Extract<keyof P, keyof D>>>
& Partial<Pick<D, Exclude<keyof D, keyof P>>>
: never;
// Simplified version of ReactManagedAttributes (we don't care about propTypes)
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/38be717/types/react/index.d.ts#L2798-L2804
type DefaultizedProps<C, P> = C extends { defaultProps: infer D }
? Defaultize<P, D>
: P;
It's a naive implementation which satisfies my needs but isn't suitable for this package. A proper implementation will IMO need to take into account _refs_ (see ComponentPropsWithRef and ComponentPropsWithoutRef). Also, I'm not sure if for this use case makes sense to merge Props type parameter with the type of propTypes as in ReactManagedAttributes etc. So if someone more skilled could pick this up…?
I reckon that having a type like this directly in @types/react package would significantly help to reduce confusions about components with defaultProps.
We were discussing something like ComponentProps and ComponentOuterProps.
@ferdaber Hi. What is the current status of this problem ? I used React.ComponentProps< typeof Component > a lot and have a lot of troubles with it (
React.ComponentProps<typeof Component> does not work with statically declared default props and generic props) - Info
``js
// 💥 Property 'y' is missing
const componentProps: React.ComponentProps<typeof Component> = {x: 'X'};
const c = <Component {...componentProps} />;
Here's what I currently use:
export type Component =
| keyof JSX.IntrinsicElements
| React.JSXElementConstructor<any>;
export type HasDefaultPropsDefined = Component & {
defaultProps: Record<string, any>;
};
export type ComponentWithDefaultsOptional<
C extends HasDefaultPropsDefined
> = Omit<React.ComponentProps<C>, keyof C["defaultProps"]> &
Partial<Pick<React.ComponentProps<C>, keyof C["defaultProps"]>>;
export type ComponentOuterProps<
C extends Component
> = C extends HasDefaultPropsDefined
? ComponentWithDefaultsOptional<C>
: React.ComponentProps<C>;
Use like ComponentOuterProps<typeof ComponentName>.
* Bump *
To be honest, I'd expect React.ComponentProps<...> to return the props the given type expects to be passed in, meaning it should respect defaultProps. What is the rationale behind not doing so?
Most helpful comment
* Bump *
To be honest, I'd expect
React.ComponentProps<...>to return the props the given type expects to be passed in, meaning it should respectdefaultProps. What is the rationale behind not doing so?