typescript-react-app-kickstart-guide
typescript-react-app-kickstart-guide copied to clipboard
question: Why use this custom way to pick default props?
currently default props are defined with
export type PickDefaultProps<Props, defaultPropsKeys extends keyof Props> = Readonly<Required<{
[P in defaultPropsKeys]: Props[P]
}>>;
and used like
type DefaultProps = PickDefaultProps<ButtonProps, 'state' | "size">;
why not do:
type DefaultProps = Partial<ButtonProps>;
using the built in Partial
type?
All the best, Sebastian
This is a nice one 🙂
Using the built in Partial
was my first intuition too and I used it in the first iteration.
The Problem:
Setting the type of Component.defaultProps
to Partial<ComponentProps>
set's the Props interface of that component to Partial<ComponentProps>
. This means everything becomes optional, even the props that are undefined
in defaultProps: Partial<ComponentProps>
.
Consider this example:
JSX internally uses Defaultize
to determine which Props are required or optional when we use the component (<Button ... />
).
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;
Here I define the Button props and the two ways of typing the default props (notice the occurrences of undefined
):
interface ButtonProps {
size: 'small' | 'medium' | 'large';
isActive: boolean;
optionalProp?: number;
}
/*
type ButtonProps = {
size: 'small' | 'medium' | 'large';
isActive: boolean;
optionalProp?: number | undefined;
}
*/
type PartialButtonProps = Partial<ButtonProps>;
/*
type PartialProps = {
size?: "small" | "medium" | "large" | undefined;
isActive?: boolean | undefined;
optionalProp?: number | undefined;
}
*/
type DefaultButtonProps = PickDefaultProps<ButtonProps, 'isActive'>;
/*
type DefaultProps = {
isActive: boolean;
}
*/
I know it's a lot of code, but bear with me 🐻.
Finally let's try to define objects that represent a 'defaultized' interface for the ButtonComponent
when used in JSX:
// valid, because 'optionalProp' is optional
const buttonProps: Defaultize<ButtonProps, never> = {
size: 'large',
isActive: false,
};
// valid! Because everything is set to optional and is allowed to be undefined
const partialButtonProps: Defaultize<ButtonProps, PartialButtonProps> = {};
// valid! This is what we actually wanted
// Only size is required, because 'optionalProp' is optional and 'isActive' has a default
const DefaultButtonProps: Defaultize<ButtonProps, DefaultButtonProps> = {
size: 'medium'
};
I'm not completely happy with the PickDefaultProps
solution. It bugs me that I have to define something twice (here being the picking and defining of the prop I want to set a default for).
It's an chicken-egg-situation. I want to type check the default props, so I have to tell TS what it has to expect.
I found the solution myself and couldn't find anything online. This is the only way I know of and I'm actually curious why no one else is complaining. Did I miss something?
I wanted to have another go at this problem (and make my solution public, so others can use it) but did not find the time to do it. Maybe you have an idea?
For the time being the PickDefaultProps
is good enough 😉.
extremely informative - thank you :)
Another way to type Components is to let the implementation dictate the type of it's defaultProps
:
type PropsWithoutDefaults = {
size: 'small' | 'medium' | 'large',
optionalProp?: number,
};
const defaultProps = {
isActive: true,
};
type ComponentProps = PropsWithoutDefaults & typeof defaultProps;
class Component extends React.PureComponent<ComponentProps> {
defaultProps = defaultProps;
}
For now I personally prefer to let the type dictate the implementation. I know a lot of folks that would disagree, though 😉.