Skip to content

Stateless components and defaultProps bug #27425

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

Closed
goloveychuk opened this issue Sep 28, 2018 · 13 comments · Fixed by #27627
Closed

Stateless components and defaultProps bug #27425

goloveychuk opened this issue Sep 28, 2018 · 13 comments · Fixed by #27627
Assignees
Labels
Bug A bug in TypeScript

Comments

@goloveychuk
Copy link

TypeScript Version: 3.2.0-dev.20180927

Search Terms:

Code

interface Props {
  text: string;
}

function BackButton(props: Props) {
  return <div/>
}
BackButton.defaultProps = {
  text: 'Go Back',
};
let a = <BackButton /> // error: text is missing in type {}

It works ok with React.Component. Also I tried to make LibraryManagedAttributes<C,P>={c: string} . This changed checking for Component, but not for SFC.

@DanielRosenwasser DanielRosenwasser added the Bug A bug in TypeScript label Sep 29, 2018
@DanielRosenwasser DanielRosenwasser added this to the TypeScript 3.2 milestone Sep 29, 2018
@chibicode
Copy link

From the typescript 3.0 release notes:

For stateless function components (SFCs) use ES2015 default initializers for SFCs:

import React from 'react'

interface Props {
    name: string;
}

function Greet({ name = "world" }: Props) {
    return <div>Hello ${name.toUpperCase()}!</div>;
}

However if I copy paste the above into an editor and run tsc with TypeScript 3.1.1 I get this error:

error TS2322: Type '{}' is not assignable to type 'Props'.
  Property 'name' is missing in type '{}'.

export const foo = <Greet />
                    ~~~~~

@chibicode
Copy link

chibicode commented Oct 1, 2018

Also, separate concern: if I'm not mistaken, ES2015 default initializers are less performant than defaultProps. If you're recommending ES2015 default initializers, maybe it should mention that it's less performant?

Source: jsx-eslint/eslint-plugin-react#1009 (comment)

There's no need for a link - by spec, default values are re-evaluated on every invocation of the function. defaultProps, on the other hand, are evaluated once and cached at the module level - across every invocation of the function. Even if React does nothing with regard to them (which I doubt, but haven't bothered to dig up evidence of), defaultProps are more performant than default values.

@chibicode
Copy link

Ah, my apologies - I just realized that defaultProps is now handled in TypeScript 3.1 (so what @goloveychuk reported is indeed a bug):
https://blogs.msdn.microsoft.com/typescript/announcing-typescript-3-1/#easier-properties-on-function-declarations

screen shot 2018-10-02 at 1 38 26 pm

@mbrowne
Copy link

mbrowne commented Nov 19, 2018

This doesn't seem to work very well in conjunction with React.forwardRef...you can use typeof as a workaround but it certainly isn't ideal:

interface Props {
    text: string
}

function BackButton(props: Props) {
    console.log('props: ', props)
    return <div />
}
BackButton.defaultProps = {
    text: 'Go Back',
}

const Wrapper = React.forwardRef<HTMLDivElement, Props>((props: Props, ref) => (
    <div ref={ref}>
        <BackButton {...props} />
    </div>
))

const Test = Wrapper as typeof BackButton & React.ClassAttributes<HTMLDivElement>

const a = <Test />

@weswigham
Copy link
Member

I think it'll work like you want it to if you use React.forwardRef<HTMLDivElement, JSX.LibraryManagedAttributes<typeof BackButton, Props>(...) instead of (incorrectly) just asserting it takes a Props (when it really takes a Props-less-the-defaults-specified-on-BackButton)

@Hotell
Copy link

Hotell commented Nov 20, 2018

While this was fixed for "raw" functional components:

import React from 'react'

type Props = { name: string }
const Hello = ({name}:Props) => <div>Hello {name}!</div>
Hello.defaultProps = { name: 'Johny Five' }


const Test = () => <Hello />

Issue still remains when using generic FunctionalComponent to annotate a component

import React, {FunctionalComponent} from 'react'

type Props = { name: string }
const Hello: FunctionalComponent<Props> = ({name}) => <div>Hello {name}!</div>
Hello.defaultProps = { name: 'Johny Five' }

// Error! missing name
const Test = () => <Hello />

@weswigham
Copy link
Member

When you use the FunctionalComponent annotation, you're stating that your component has an optional defaultProps of type Partial<P> - the more specific type you assign to the field doesn't do anything, since you already annotated it. You need to include the specific defaultProps type you assign in the type annotation.

@Hotell
Copy link

Hotell commented Nov 22, 2018

Right, so we can do following change in react types or user needs to explicitly define default props:

type Props = { name: string }

const Hello: FunctionalComponent<Props> & {defaultProps: Partial<Props>} = ({name}) => <div>Hello {name}!</div>

Hello.defaultProps = { name: 'Johny Five' }

// No Error
const Test = () => <Hello />

@cheeZery
Copy link

I know this is issue is already closed, but imho the problem still exists. And although @Hotell provided a solid work around, which solves the problem, it also introduces another problem in which TypeScript just treats every prop of the function component as optional, even if there's no default prop given.

type Props = {
  name: string;
  birthday: Date;
}

const Hello: FunctionalComponent<Props> & {defaultProps: Partial<Props>} = ({name, birthday}) => {
  return <div>Hello {name}! Your birthday is {birthday.toLocaleDateString()}</div>
}

Hello.defaultProps = { name: 'Johny Five' }

const Test = () => <Hello />

The last line would still show no error but the rendering will obviously fail with Cannot read property 'toLocaleDateString' of undefined.

You'd need to use a more specific type when declaring Hello, which would still just be a work around.
The actual issue in TypeScript remains.

@dragomirtitian
Copy link
Contributor

@cheeZery The current definitions for react include {defaultProps?: Partial<Props>} directly in the FunctionComponent, which I think is unfortunate. This makes it very difficult to make just a subset of props considered for defaultProps. Since Defaultize in react libs considers just keyof D (ie keyof defaultProps). Once you make defaultProps required (with the intersection), all the keys of Partial<Props> will be considered in Defaultize when figuring out what keys are in the defualt. You don't even need any prop names in the intersection, this will exhibit the same behavior: const Hello: FunctionComponent<Props> & { defaultProps: {} }.

We can get things to work as expected if we do some surgery on FunctionComponent to remove the existing defaultProps. Also I would define defaultProps beforehand and use the type of the const:

type Props = {
    name: string;
    birthday: Date;
}
type FixDefaults<T extends FunctionComponent<any>, D extends Partial<ComponentProps<T>>> = Pick<T, Exclude<keyof T, 'defaultProps'>>  // remove defaultProps
    & (T extends (...a: infer A)=> infer R? (...a: A)=> R : never) // keep signature
    & { defaultProps: D } // new defaults 

const defaultProps = { name: 'Johny Five' };
    
const Hello: FixDefaults<FunctionComponent<Props>, typeof defaultProps> = ({ name, birthday }) => {
    return <div>Hello {name}! Your birthday is {birthday.toLocaleDateString()}</div>
}

Hello.defaultProps = defaultProps

const Test1 = () => <Hello />// Err
const Test2 = () => <Hello birthday={new Date()} /> // ok

Or define a helper function that keeps default definition and assignment in one place:

type Props = {
    name: string;
    birthday: Date;
}
type FixDefaults<T extends FunctionComponent<any>, D extends Partial<ComponentProps<T>>> = Pick<T, Exclude<keyof T, 'defaultProps'>>  // remove defaultProps
    & (T extends (...a: infer A)=> infer R? (...a: A)=> R : never) // keep signature
    & { defaultProps: D } // new defaults 

function declareWithDefaults<TProps>() {
    return function<TDefualts extends Partial<TProps>>(comp: FunctionComponent<TProps>, defaultProps: TDefualts): FixDefaults<FunctionComponent<TProps>, TDefualts> {
        comp.defaultProps = defaultProps;
        return comp as any
    }
}

const Hello = declareWithDefaults<Props>()(({ name, birthday }) => {
    return <div>Hello {name}! Your birthday is {birthday.toLocaleDateString()}</div>
}, { name: 'Johny Five' });

const Test1 = () => <Hello />// Err
const Test2 = () => <Hello birthday={new Date()} /> // ok

Although I understand defining extra functions just to get the types to work out is a bit controversial :)

@lukewlms
Copy link

lukewlms commented Mar 29, 2019

Update:

defaultProps on functions will eventually go away according to Dan Abramov. The right way to do default props on functional components is just to use default function parameters.


Old answer:

The workaround of just not specifying any type at all on components and letting it be inferred seems to cause everything to work as desired.

const MyComponent = (props: { name: string, telephone: string }) => {
  ...
}

MyComponent.defaultProps = { telephone: "222-333-4444" }

// Works - good
const test = <MyComponent name="Hulk Hogan" />

// Doesn't work - missing name, good
const test = <MyComponent />

(As noted by @cheeZery , the previous workaround causes all props to be considered optional, which won't work for our case and I suspect most others.)

@bfricka
Copy link

bfricka commented Nov 11, 2019

@lukewlms I think you missed the discussion where @Hotell described how "raw" FC infer correctly, but this breaks in cases where the component can't be "raw" because it has types attached to it, e.g. forwardRef.

type FooProps = { name: string, telephone: string };
const MyComponent = (props: FooProps) => null;
MyComponent.defaultProps = {telephone: '222-333-4444'};

// Works
<MyComponent name="Hulk Hogan"/>

const FwdMyComponent = React.forwardRef((props: FooProps, ref: Ref<any>) => null);
FwdMyComponent.defaultProps = {telephone: '222-333-4444'};

// Doesn't work
<FwdMyComponent name="Terry"/>;

@rajington
Copy link

rajington commented Apr 10, 2020

Partial application in React is useful for extending components, since defaultProps is going away could this work (at least for React.FCs)?:

import * as React from "react";

// withDefault helper type
const withDefault = <
  Component extends React.FC<any>,
  Props extends React.ComponentProps<Component>,
  DefaultProps extends keyof Partial<Props>
>(
  component: Component,
  defaultProps: Pick<Props, DefaultProps>
): React.FC<
  Omit<Props, DefaultProps> & Partial<Pick<Props, DefaultProps>>
> => props => React.createElement(component, { ...defaultProps, ...props });

// example base component
const Base: React.FC<{
  foo: string;
  bar: string;
}> = ({ foo, bar }) => (
  <pre>
    {foo} {bar}
  </pre>
);

// extended component
const DefaultFoo = withDefault(Base, { foo: "default" });
const DefaultBar = withDefault(Base, { bar: "default" });

// Errors
// const ExtraDefault = withDefault(Base, { qox: "default" });

export default () => (
  <>
    <Base foo="manual" bar="manual" />
    <DefaultFoo bar="manual" />
    <DefaultBar foo="manual" />
    {/* can override defaults */}
    <DefaultFoo foo="manual" bar="manual" />
    <DefaultBar foo="manual" bar="manual" />
  </>
);

output:

manual manual
default manual
manual default
manual manual
manual manual

playground link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.