[react]: defaultProps doesn't work when FunctionalComponent<P> is used
Hotell opened this issue · comments
- I tried using the
@types/react
package and had problems. - I tried using the latest stable version of tsc. https://www.npmjs.com/package/typescript
- I have a question that is inappropriate for StackOverflow. (Please ask any appropriate questions there).
- Mention the authors (see
Definitions by:
inindex.d.ts
) so they can respond.- Authors: @ferdaber @johnnyreilly @sandersn @RyanCavanaugh @tkrotoff @Kovensky @weswigham
Dependencies:
"typescript": "3.2.0-rc"
"@types/react": "16.7.6",
"@types/react-dom": "16.0.9",
As stated by @Kovensky
defaultProps
doesn't properly apply on JSX tag when function component is defined via FunctionalComponent
Code:
import React, { FunctionComponent } from 'react'
type Props = {
onClick: (ev: import('react').MouseEvent<HTMLElement>) => void
children: import('react').ReactChild
color: 'red' | 'green'
}
const Button: FunctionComponent<Props> = ({ onClick: handleClick, color, children }) => (
<button style={{ color }} onClick={handleClick}>
{children}
</button>
)
Button.defaultProps = {
color: 'red'
}
const Test = () => (
<>
/* $ExpectError */
<Button onClick={() => console.log('clicked')}>Click me</Button>
</>
)
Related:
#29816
microsoft/TypeScript#27425
FunctionComponent
states that Button
has a defaultProps
of Partial<Props>
; the more specific type you then assign is lost. You'd need to include the more specific default type in your type annotation. That comment in react refers to a bug where we didn't push SFCs thru the LibraryManagedAttributes
pipeline at all that's already fixed in nightly.
We should ideally be removing that property then from the FunctionComponent
interface, as it's not all that useful anyways as far as type definitions go.
It's not wrong or anything - it's just when you explicitly write an annotation like that it overrides the inferred type - removing the property wouldn't help ya, you'd just get a type error on the default props assignment then.
Well, now that's a problem......
Removing the annotation from FunctionComponent won't allow you to attach the defaultProps at all if what you have is already of FunctionComponent type.
Moreover, when attaching defaultProps to function declarations (TS 3.1 feature?), you won't get any type checking at all on your default props object until (this is fixed and) you try to use it, and even then there won't be excess property checks.
Note that I did attempt to write a test even with the "should work" function declaration case. JSX.LibraryManagedAttributes does extract the correct type, but still it doesn't work in JSX. The tests are commented out in DT because the React types are still 2.8-compatible.
Moreover, when attaching defaultProps to function declarations (TS 3.1 feature?)
from my experience, I stopped to use FunctionComponent definition ( former SFC ) long time ago. Instead, following pattern covers everything that user needs to define a function component with default props:
type Props = { age: number } & typeof defaultProps;
const defaultProps = {
who: 'Johny Five',
};
const Greet = (props: Props) => {
/*...*/
};
- It covers aforementioned issue (your default props wont be checked until the component is used - with this pattern they are checked as you worked with them within component implementation)
- It allows generic component definition
- enforces to explicitly define
children
- idiomatic TS/JS (annotating just arguments as normal function - easier for newcomers)
more info in my post https://medium.com/@martin_hotell/10-typescript-pro-tips-patterns-with-or-without-react-5799488d6680 ( points 8. and 9. )
FunctionalComponent
should be used only as annotation for HoC if needed.
Fix suggestion:
We can transform FunctionComponent, with addition of 2nd generic argument, which would define static props that are present:
interface FunctionComponentStatics<P = {}> {
propTypes: React.ValidationMap<P>;
contextTypes: React.ValidationMap<any>;
defaultProps: Partial<P>;
displayName: string;
}
type FixFunctionComponent<
P extends object = {},
StaticKeys extends keyof FunctionComponentStatics<P> = never
> = {
(
props: P & { children?: React.ReactNode },
context?: any
): React.ReactElement<any> | null;
} & { [K in StaticKeys]: FunctionComponentStatics<P>[K] };
// usage:
type Props = {
onClick: (ev: import('react').MouseEvent<HTMLElement>) => void;
children: import('react').ReactChild;
color: 'red' | 'green';
};
const Button: FixFunctionComponent<Props, 'defaultProps'> = ({
onClick: handleClick,
color,
children,
}) => (
<button style={{ color }} onClick={handleClick}>
{children}
</button>
);
// if this would be missing we would get compile error
// also intellisense and excess property checks works
Button.defaultProps = {
color: 'red',
};
const Test = () => (
<>
/* No Errors 👌, color is optional */
<Button onClick={handleClick}>Click me</Button>
</>
)
WDYT ? IF agreed I'll send an PR
thanks !
I would definitely enjoy having something like the proposal above as this is becoming more of an issue as hooks are adopted at my work.
The current FunctionComponent
already gives you type checking for both propTypes
and defaultProps
, you just have to annotate it when declaring. The above solution only provides you to partially define some of the static properties.
FunctionComponent
provides type checking for declaring defaultProps
/ propTypes
but it is completely incompatible with LibraryManagedAttributes
.
My advice is to not use the FunctionComponent
type at all; Hotell's pattern is a better approach overall.
I can't quite decide myself if it's better to declare components as function
or () =>
, though; they both have pros and cons (() =>
closes over the outer this
and arguments
, function
has a prototype
object, etc). For now, function
being hoisted is my only winning criteria.
Oh derp. I even advised against that in the cheatsheet. Too many things to track 😅
I think understand the problem here, however I really wish there was a way to have both FunctionComponent
and a well typed defaultProps
.
Generally speaking, I always prefer to annotate functions with their formal types as opposed to manually annotating the parameters and return types, because the function type already describes and validates the whole function (parameters + return type), and it can also validate any properties on the function, such as displayName
. Unfortunately however it seems there's no way forward for both FunctionComponent
and a well typed defaultProps
.
There is also another scenario where annotating the whole function is problematic: generic components.
import * as React from 'react';
import { ReactElement } from 'react';
type Props<T> = { t: T };
// Since we have no way of passing the generic into the `FC` function type annotation,
// we have to write out the full function type
const MyComponent = <T>(props: Props<T>): ReactElement<any> | null =>
<div>Hello, World!</div>
There's some discussion around a fix for that in microsoft/TypeScript#27124, which could also help with the scenario here.
Leaving the optionality of defaultProps
aside, this is the big limitation that makes it difficult for LibraryManagedAttributes
to work with static properties of callables, which is that we can't "mutate" the type of a property or a variable:
type Props = {
foo: string
bar: boolean
}
const MyComponent: React.FC<Props> = props => <div />
MyComponent.defaultProps = {
// bar is autocompleted here because of the type of `MyComponent.defaultProps`
bar: true
}
// past the above statement, however, the type of
// `MyComponent.defaultProps` is still Partial<Props> instead of { bar: boolean }
Just linking here relevant RFC which is deprecating defaultProps
so this issue might be deprecated as well when it goes through ... reactjs/rfcs#107
Hi thread, we're moving DefinitelyTyped to use GitHub Discussions for conversations the @types
modules in DefinitelyTyped.
To help with the transition, we're closing all issues which haven't had activity in the last 6 months, which includes this issue. If you think closing this issue is a mistake, please pop into the TypeScript Community Discord and mention the issue in the definitely-typed
channel.