-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Comments
From the typescript 3.0 release notes:
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
|
Also, separate concern: if I'm not mistaken, ES2015 default initializers are less performant than Source: jsx-eslint/eslint-plugin-react#1009 (comment)
|
Ah, my apologies - I just realized that |
This doesn't seem to work very well in conjunction with 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 /> |
I think it'll work like you want it to if you use |
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 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 /> |
When you use the |
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 /> |
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 You'd need to use a more specific type when declaring |
@cheeZery The current definitions for react include We can get things to work as expected if we do some surgery on 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 :) |
Update:
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.
(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.) |
@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. 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"/>; |
Partial application in React is useful for extending components, since 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:
|
TypeScript Version: 3.2.0-dev.20180927
Search Terms:
Code
It works ok with
React.Component
. Also I tried to makeLibraryManagedAttributes<C,P>={c: string}
. This changed checking forComponent
, but not forSFC
.The text was updated successfully, but these errors were encountered: