typescript-react-app-kickstart-guide icon indicating copy to clipboard operation
typescript-react-app-kickstart-guide copied to clipboard

question: Why use this custom way to pick default props?

Open skurfuerst opened this issue 6 years ago • 3 comments

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

skurfuerst avatar Nov 17 '18 15:11 skurfuerst

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 😉.

JamesAlias avatar Nov 17 '18 18:11 JamesAlias

extremely informative - thank you :)

skurfuerst avatar Nov 17 '18 18:11 skurfuerst

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 😉.

JamesAlias avatar May 23 '19 09:05 JamesAlias