Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React component props: mixing defaultProps and union types #25503

Closed
OliverJAsh opened this issue Jul 8, 2018 · 4 comments
Closed

React component props: mixing defaultProps and union types #25503

OliverJAsh opened this issue Jul 8, 2018 · 4 comments
Labels
Domain: JSX/TSX Relates to the JSX parser and emitter In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@OliverJAsh
Copy link
Contributor

TypeScript Version: 2.9.2

Search Terms: react component props union types defaultprops

Code

Using the new support for defaultProps in TypeScript 3.0 re. #24422

{
    type Props = { shared: string } & (
        | {
              type: 'foo';
              foo: string;
          }
        | {
              type: 'bar';
              bar: string;
          });
    class Component extends ReactComponent<Props> {
        static defaultProps = {
            shared: 'yay',
        };
    }

    <Component type="foo" />; // Expected error, got none
    // Unexpected error
    // Property 'foo' does not exist on type 'Defaultize<Props, { shared: string; }>'.
    <Component type="foo" foo="1" />;
}

#24422 (comment)

/cc @mhegazy who requested that I open an issue to discuss/track this.

@OliverJAsh
Copy link
Contributor Author

I'm assuming this is because the way "outer props" type of a component is calculated uses keyof somewhere along the line, and keyof only return the common properties in a union type? I've ran into this before when trying to use a withDefaults HOC that did something similar.

@OliverJAsh
Copy link
Contributor Author

OliverJAsh commented Jul 19, 2018

This issue doesn't just effect components with default props, but also functions that have named props with defaults.

Here is a complete example that demonstrates the problem. I'm using functions here instead of components for simplicity. (Components are just functions.)

I think what we're missing is something like Diff (also known as Defaultize helper) that preserves the union properties. Is there a proposal somewhere for something like that?

// `Omit` and `Diff` copied from
// https://github.com/gcanti/typelevel-ts/blob/e1d718db887476f7b6b6e2c88542cc6ce3bd8265/src/index.ts#L9-L13
type Omit<A extends object, K extends string | number | symbol> = Pick<A, Exclude<keyof A, K>>;
type Diff<A extends object, K extends keyof A> = Omit<A, K> & Partial<Pick<A, K>>;

type Props = { shared: string } & (
    | {
          type: 'foo';
          foo: string;
      }
    | {
          type: 'bar';
          bar: string;
      });

const defaultProps = {
    shared: 'yay',
};
type DefaultProps = typeof defaultProps;

type InputProps = Diff<Props, keyof DefaultProps>;

const component = (props: Props) => {};

const componentWithDefaults = (inputProps: InputProps) => {
    /* Type '{ type: "foo" | "bar"; shared: string; }' is not assignable to type 'Props'.
        Type '{ type: "foo" | "bar"; shared: string; }' is not assignable to type '{ shared: string; } & { type: "bar"; bar: string; }'.
            Type '{ type: "foo" | "bar"; shared: string; }' is not assignable to type '{ type: "bar"; bar: string; }'.
            Property 'bar' is missing in type '{ type: "foo" | "bar"; shared: string; }'. */
    const props: Props = { ...defaultProps, ...inputProps }

    return component(props);
};

componentWithDefaults({
    type: 'foo',
    /* Argument of type '{ type: "foo"; foo: string; }' is not assignable to parameter of type 'Diff<Props, "shared">'.
        Object literal may only specify known properties, and 'foo' does not exist in type 'Diff<Props, "shared">'. */
    foo: 'foo',
})

@OliverJAsh
Copy link
Contributor Author

Ah, this is possible to fix if we rewrite the Omit type to use conditional types, which distribute over unions:

type Omit<T, K extends keyof T> = T extends any ? Pick<T, Exclude<keyof T, K>> : never

#12215 (comment)

Using this knowledge we can rewrite Defaultize to use conditional types:

type Defaultize<TProps, TDefaults> = TProps extends any
    ? { [K in Extract<keyof TProps, keyof TDefaults>]?: TProps[K] } &
          { [K in Exclude<keyof TProps, keyof TDefaults>]: TProps[K] } &
          Partial<TDefaults>
    : never;

Then we get the expected behaviour:

    <Component type="foo" />; // Expected error, got one :-)
    <Component type="foo" foo="1" />; // No error, as expected :-)

@OliverJAsh
Copy link
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: JSX/TSX Relates to the JSX parser and emitter In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

2 participants