DefinitelyTyped / DefinitelyTyped

The repository for high quality TypeScript type definitions.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[react]: defaultProps doesn't work when FunctionalComponent<P> is used

Hotell opened this issue · comments


Dependencies:

"typescript": "3.2.0-rc"
"@types/react": "16.7.6",
"@types/react-dom": "16.0.9",

As stated by @Kovensky

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/test/managedAttributes.tsx#L167-L175

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?)

Yup

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.