Definitelytyped: Implementing defaultProps with ts 2.0 strict null checks

Created on 30 Sep 2016  ·  50Comments  ·  Source: DefinitelyTyped/DefinitelyTyped

Default properties don't seem to function well currently with strictNullChecks enabled. For example:

interface TestProps { x?: number}

class Test extends React.Component<TestProps, null> {

    static defaultProps =  {x: 5};

    render() {
        const x: number = this.props.x;
        return <p>{x}</p>;
    }
}

Errors with error TS2322: Type 'number | undefined' is not assignable to type 'number' even though this is guaranteed to work at runtime.

Right now defaultProps and Props seem to be treated as the same type always, but they're actually almost never the same type because optional fields in Props should be overwritten by required values in DefaultProps.

What if there were something like...

class ComponentWithDefaultProps<P, D, S> {
    props: P & D & {children?: React.Children};
}

that is identical to the existing React.Component typing except for the type of props?

Most helpful comment

If anyone has a good solution for types and defaultProps, I'm all ears. We currently do this:

interface Props {
  firstName: string;
  lastName?: string;
}

interface DefaultProps {
  lastName: string;
}

type PropsWithDefaults = Props & DefaultProps;

export class User extends React.Component<Props> {
  public static defaultProps: DefaultProps = {
    lastName: 'None',
  }

  public render () {
    const { firstName, lastName } = this.props as PropsWithDefaults;

    return (
      <div>{firstName} {lastName}</div>
    )
  }
}

All 50 comments

Since the default props are set at runtime, I'm not sure if there's a way to handle this nicely, other than a type assertion. (Of course, you could always just disable strict null checks.)

Here's how you may be able to get around it in your example:

interface TestProps { x?: number}

class Test extends React.Component<TestProps, null> {

    static defaultProps =  {x: 5};

    render() {
        const x: number = (this.props.x as number);
        return <p>{x}</p>;
    }
}

See https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions

If there's a more graceful way of handling this, I'd love to hear it.

Disclaimer: I've been using TypeScript for about three days, I'm tired, and probably have no idea what I'm talking about.

+1000 on updating type definition file to include three generic type definitions.

This used to be fine without ---strictNullChecks, but now, it is definitely going to be a problem for many component classes.

Flow also implements similar class implementation because of nature of strict null type checking.
https://github.com/facebook/flow/blob/master/lib/react.js#L16
https://github.com/facebook/flow/blob/master/lib/react.js#L104-L105

Seems like we don't have much options here except of waiting for https://github.com/Microsoft/TypeScript/issues/2175 being resolved in order to add third generic.
I don't think that such a (breaking) change (I mean class Component<P, S, D>) can be approved by reviewers.
@johnnyreilly @bbenezech @pzavolinsky do you guys have an opinion on that?

@r00ger agreed. Changing the definition is too disruptive.

Has anyone considered using Partial?

As in:

    interface ComponentClass<P> {
-        defaultProps?: P;
+        defaultProps?: Partial<P>;
    }

Don't mind that Partial stuff above.

Partial only solves how to declare partial propTypes problem. Inside of render, lastName is still of type string | undefined. To get around it, you have to cast it string using as or ! like shown below. It works, but is not ideal.

interface IUser {
    firstName: string
    lastName?: string
}
export class User extends React.Component<IUser, {}> {
    public static defaultProps: Partial<IUser> = {
        lastName: 'None',
    }

    public render () {
        const { firstName, lastName } = this.props
        // error
        lastName.toUpperCase()

        return (
            <div>{firstName} {lastName}</div>
        )
    }
}

I've just started using TS. Am I missing anything?

If anyone has a good solution for types and defaultProps, I'm all ears. We currently do this:

interface Props {
  firstName: string;
  lastName?: string;
}

interface DefaultProps {
  lastName: string;
}

type PropsWithDefaults = Props & DefaultProps;

export class User extends React.Component<Props> {
  public static defaultProps: DefaultProps = {
    lastName: 'None',
  }

  public render () {
    const { firstName, lastName } = this.props as PropsWithDefaults;

    return (
      <div>{firstName} {lastName}</div>
    )
  }
}

+1
I am currently battling this issue.

+1

+1

In addition to adding the third type parameter, you'll need the ability to diff props against the defaulted props. Happily as of TS 2.4 this is now possible! See https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-319495340

IMHO adding third parameter is a big no no, also Flow team knew that and recently they changed that for greater good. It should be type-checker responsibility to know how to handle these kind of things.

Don't get me wrong, I love Typescript, but since Flow 0.53 I have to say it's superior for React development https://medium.com/flow-type/even-better-support-for-react-in-flow-25b0a3485627

@Hotell Flow does have three type parameters for React.Component - per the Medium article you linked to Flow can infer class type parameters from the subclass annotations - a neat language-level feature TS doesn't support, but not a type-declaration consideration AFAIK.

@aldendaniels

Flow does have three type parameters for React.Component

nope, it used to be that way before 0.53, not anymore :) https://github.com/facebook/flow/commit/20a5d7dbf484699b47008656583b57e6016cfa0b#diff-5ca8a047db3f6ee8d65a46bba4471236R29

@Hotell Ah, sure enough! Thanks for correcting me.

AFAIK there's no way in TS to infer the type of the default props though. Using the three-type-parameter approach we'd likely be able to get correct typing without blocking on upstream changes from the TypeScript team.

Do you know of a way to use the inferred type of a static property without passing typeof MyComponent.defaultProps as a type param?

Any news on this subject? Does someone do a PR to add a third type parameter and use https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-319495340?

Upvoting issue: same problem

+1

I also ran into this, and I've chosen (until this is properly fixed) to abstain from using static defaultProps and instead use a helper HOC:

File components/helpers/withDefaults.tsx:

import * as React from 'react'

export interface ComponentDefaulter<DP> {
  <P extends {[key in keyof DP]?: any}>(Component: React.ComponentType<P>): React.ComponentType<
    Omit<P, keyof DP> &         // Mandate all properties in P and not in DP
    Partial<Pick<P, keyof DP>>  // Accept all properties from P that are in DP, but use type from P
  >
}

export default function withDefaults<DP>(defaultProps: DP): ComponentDefaulter<DP> {
  return Component => props => <Component {...defaultProps} {...props}/>
}

Now I can use:

File components/Button.tsx:

import * as React from 'react'
import withDefaults from './helpers/withDefaults'

export interface ButtonProps {
  label: string
  onPress: () => any
}

export const defaultProps = {
  onPress: () => undefined
}

class Button extends React.Component<ButtonProps> {
  // ...
}

export default withDefaults(defaultProps)(Button)

Three potential downsides (that I can think of):

  1. It requires an HOC, but since this is a quite common paradigm in React world, that seems OK.
  2. You have to declare the props as a generic type parameter, and cannot rely on inference from the props property.
  3. There is no implicit checking of the types of defaultProps, but this can be remedied by specifying export const defaultProps: Partial<ButtonProps> = {...}.

According to @vsaarinen, I write a base class with props: Props & DefaultProps, so the whole class that extends the base class can directly use this.props without use this.props as PropsWithDefaults.

Like this:

import * as React from 'react'

export class Component<P = {}, S = {}, DP = {}> extends React.Component<P, S> {
  props: Readonly<{ children?: React.ReactNode }> & Readonly<P> & Readonly<DP>
}

export interface Props {
  firstName: string
  lastName?: string
}

export interface DefaultProps {
  lastName: string
}

export class User extends Component<Props, any, DefaultProps> {
  render() {
    const { firstName, lastName } = this.props

    // no error
    return (
      <div>{firstName} {lastName.toUpperCase()}</div>
    )
  }
}

Actually @qiu8310 that didnt work fully, Still had issues with call sites screaming about those default props not being optional. Got it to work with a minor adjustment

import * as React from 'react'

export class Component<P = {}, S = {}, DP = {}> extends React.Component<P, S> {
  // Cast the props as something where readonly fields are non optional
  props = this.props as Readonly<{ children?: React.ReactNode }> & Readonly<P> & Readonly<DP>
}

export interface Props {
  firstName: string
  lastName?: string
}

export interface DefaultProps {
  lastName: string
}

export class User extends Component<Props, any, DefaultProps> {
  render() {
    const { firstName, lastName } = this.props

    // no error
    return (
      <div>{firstName} {lastName.toUpperCase()}</div>
    )
  }
}

I played with third generic and ended up having something similar to @qiu8310's proposal:

// ComponentWithDefaultProps.ts
import * as React from "react";

export declare class ComponentWithDefaultProps<P, S, DP extends Partial<P>> extends React.Component<P & DP, S> {}
type redirected<P, S, DP> = ComponentWithDefaultProps<P, S, DP>;
const redirected: typeof ComponentWithDefaultProps = React.Component as any;

export const Component = redirected;

// User.ts
import { Component } from "ComponentWithDefaultProps";
export interface Props {
  firstName: string
  lastName?: string
}
export interface DefaultProps {
  lastName: string
}

export class User extends Component<Props, {}, DefaultProps> {
  public render() {
    const { firstName, lastName } = this.props;
    return <div>{firstName} {lastName.toUpperCase()}</div>;
  }
}

However both of these approaches (mine and the approach above) causes the bigger problem. There are types of created component in my example:

User: React.ComponentClass<P & DP>
User["props"]: Readonly<{ children?: React.ReactNode }> & Readonly<P & DP>

Apparently, the User's interface is wrong. React.ComponentClass<P & DP> means that lastName is required too, so that

<User firstName="" />;
//    ~~~~~~~~~~~~  Property 'lastName' is missing...

In the @qiu8310's example types are different:

User: React.ComponentClass<P>
User["props"]: Readonly<{ children?: React.ReactNode }> & Readonly<P> & Readonly<DP>

But the same piece of JSX causes the same error, because tsc's JSX checks are based on props' type.

<User firstName="John" />;
//    ~~~~~~~~~~~~~~~~  Property 'lastName' is missing...

Fun thing is that <User firstName="John" /> is being turned into React.createElement(User, {firstName: "John"}) which would be a is valid TypeScript. In that case type checks rely on ComponentClass first type parameter, so

<User firstName="Jonh" />; // doesn't work, but
React.createElement(User, { firstName: "John" }); // works

As you see, even having third generic we still have to add another trick in order to export a component with correct interface:

export const User = class extends Component<Props, {}, DefaultProps> {
    // ...
} as React.ComponentClass<Props>;

<User firstName="Jonh" />; // works

So having third generic doesn't make much sense.

It seems there is no good solution that can be merged to React's definition, for now I stick with using ComponentWithDefaultProps and asserting the type of exported component.

export interface DefaultProps {
    lastName: string;
}
export interface Props extends Partial<DefaultProps> {
    firstName: string;
}

export type PropsWithDefault = Props & DefaultProps;

export const User: as React.ComponentClass<Props> =
class extends React.Component<PropsWithDefault> {
    render() {
        // no error
        return <div>
            {this.props.firstName}
            {this.props.lastName.toUpperCase()}
        </div>;
    }
};
// Note, we've assigned `React.Component<PropsWithDefault>` to `React.ComponentClass<Props>`

Apart from that you can assert every usage of this.props type in component's methods (e.g. const { lastName } = this.props as Props & DefaultProps, or using exclamation mark everywhere this.props.lastName!.toLowerCase()).

i found some example about this discussion - https://github.com/gcanti/typelevel-ts#objectdiff

@rifler so called HOC approach (I prefer decorator) has been here for a while, we try to come up with a solution that do not add runtime overhead

Oh, great
Hope you will find the solution

any progress?

The following is a variation on the technique mentioned by @r00ger:

interface IUser {
    name: string;
}
const User = class extends React.Component<IUser> {
    public static defaultProps: IUser = {name: "Foo"}
    public render() {
        return <div>{this.props.name}</div>;
    }
} as React.ComponentClass<Partial<IUser>>;
React.createElement(User, {}); // no error, will output "<div>Foo</div>"

Using the above snippet will work, but you will lose the ability to use static properties on User, since it becomes an anonymous class. A hacky solution would be to shadow the class name, like so:

// tslint:disable-next-line:no-shadowed-variable
const User = class User extends React.Component<IUser>

You may now use private static fields inside the class. Public statics are still unusable. Also, notice the need to silence tslint.

I thought it worth mentioning that as of TS 2.8, the Exclude type is officially supported:

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

See https://github.com/Microsoft/TypeScript/pull/21847.

So all we need is for React.createElement() to require the following in lieu of Props:

Omit<Props, keyof DefaultProps>

The only problem is that in the React declarations, there's no DefaultProps type - for this we need either a third type parameter OR the ability to infer the type of static members as a language feature.

In the meantime, we've been rolling with the following:

/**
 * The Create type allow components to implement a strongly thed create() function
 * that alows the caller to omit props with defaults even though the component expects
 * all props to be populated. The TypeScript React typings do not natively support these.
 */
export type Create<C extends BaseComponent<any, any>, D extends {} = {}> = (
  props?: typeHelpers.ObjectDiff<C['props'], D> & React.ClassAttributes<C>,
  ...children: React.ReactNode[]
) => React.ComponentElement<any, any>;

export interface DomPropsType {
  domProps?: domProps.DomProps;
}

export class BaseComponent<P, S = {}> extends React.Component<P & DomPropsType, S> {
  static create(props?: object, ...children: React.ReactNode[]) {
    return React.createElement(this, props, ...children);
  }

  constructor(props: P & DomPropsType, context?: any) {
  ...
}

And all our components look like:

export class InsertObjectMenu extends BaseComponent<Props, State> {
  static create: Create<InsertObjectMenu, typeof InsertObjectMenu.defaultProps>;
  static defaultProps = {
    promptForImageUpload: true,
  };
  ...
}

Finally we have a lint rule enforcing that the create attribute is declared on all components. We don't use JSX, so we use:

InsertObjectMenu.create({...})

Instead of React.createElement().

We've been using this approach across a large codebase for close to a year now with good success, but we'd love to adopt JSX and this is what's holding us back.

So much time invested in this "simple issue" . I'll just leave this here https://medium.com/@martin_hotell/ultimate-react-component-patterns-with-typescript-2-8-82990c516935 🖖

    interface Component<P = {}, S = {}, DP extends Partial<P>=P> extends ComponentLifecycle<P, S> { }
    class Component<P, S, DP extends Partial<P> = P> {
        constructor(props: P & DP, context?: any);

        // We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
        // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
        // Also, the ` | S` allows intellisense to not be dumbisense
        setState<K extends keyof S>(
            state: ((prevState: Readonly<S>, props: P) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
            callback?: () => void
        ): void;

        forceUpdate(callBack?: () => void): void;
        render(): ReactNode;

        // React.Props<T> is now deprecated, which means that the `children`
        // property is not available on `P` by default, even though you can
        // always pass children as variadic arguments to `createElement`.
        // In the future, if we can define its call signature conditionally
        // on the existence of `children` in `P`, then we should  remove this.
        private __externalProps: Readonly<{ children?: ReactNode }> & Readonly<P>;
        props: Readonly<{ children?: ReactNode }> & Readonly<P> & DP;
        state: Readonly<S>;
        context: any;
        refs: {
            [key: string]: ReactInstance
        };
    }

    class PureComponent<P = {}, S = {}, DP extends Partial<P>=P> extends Component<P, S, P> { }


interface ElementAttributesProperty { __externalProps: {}; }

Look carefully at last line.

With this changes we could have

interface Props {
    a: string
    b?: string
    c?: string
}

class Comp extends React.Component<Props, {}, typeof Comp.defaultProps> {
    static defaultProps = {
        b: ''
    }

    render() {
        const {a, b, c} = this.props

        let res = a.concat(b)  // ok
        let res1 = a.concat(c) //fail

        return null
    }
}



const res1= <Comp a=''/> // ok
const res3 = <Comp /> // fail

Which is best we can get if using static defaultProps (ts checker should be changed if we want to omit typeof Comp.defaultProps).
Other options, was already said - HOC, type casts.

Here's my (very ugly) attempt based on the idea from https://medium.com/@martin_hotell/ultimate-react-component-patterns-with-typescript-2-8-82990c516935:

type ExtractProps<T> = T extends React.ComponentType<infer Q> ? Q : never;
type ExtractDefaultProps<T> = T extends { defaultProps?: infer Q } ? Q : never;
type RequiredProps<P, DP> = Pick<P, Exclude<keyof P, keyof DP>>;
type RequiredAndPartialDefaultProps<RP, DP> = Required<RP> & Partial<DP>;

type ComponentTypeWithDefaultProps<T> =
  React.ComponentType<
    RequiredAndPartialDefaultProps<
      RequiredProps<ExtractProps<T>, ExtractDefaultProps<T>>,
      ExtractDefaultProps<T>
    >
  >;

function withDefaultProps<T extends React.ComponentType<any>>(Comp: T) {
  return Comp as ComponentTypeWithDefaultProps<T>;
}
interface IProps {
  required: number;
  defaulted: number;
}

class Foo extends React.Component<IProps> {
  public static defaultProps = {
    defaulted: 0,
  };
}

// Whichever way you prefer... The former does not require a function call
const FooWithDefaultProps = Foo as ComponentTypeWithDefaultProps<typeof Foo>;
const FooWithDefaultProps = withDefaultProps(Foo);

const f1 = <FooWithDefaultProps />;  // error: missing 'required' prop
const f2 = <FooWithDefaultProps defaulted={0} />;  // error: missing 'required' prop
const f3 = <FooWithDefaultProps required={0} />;  // ok
const f4 = <FooWithDefaultProps required={0} defaulted={0} />;  // ok

@decademoon, seems like we could just use this solution at @types/react, can we? I mean, if we replace the usual React.ComponentType with your solution.
If that so, maybe you can create a PR?

@decademoon your definition doesn't handle the case where the non-default props actually include optional fields, i.e.

interface IProps {
  required: number;
  notRequired?: () => void;
  defaulted: number;
}

class Foo extends React.Component<IProps> {
  public static defaultProps = {
    defaulted: 0,
  };
}

I got it working in my case by changing your RequiredAndPartialDefaultProps type to not wrap the "RP" with "Required"

type RequiredAndPartialDefaultProps<RP, DP> = RP & Partial<DP>;

I'm surprised there still isn't a proper solution or at least a working HOC on NPM; unless I have missed something.

Hi everyone. Just wanted to say and if you are still reading this thread: I think @JoshuaToenyes made the most meaningful and helpful explanation. This is definitely not an issue so there's nothing to do with it. Use type assertion in this case.

@toiletpatrol actually, @decademoon's solution (with my slight amendment) automatically handles default props just fine. It definitely could be merged into the DT definitions for React to provide the feature standard to everyone.

@toiletpatrol @RobRendell did you see it https://github.com/Microsoft/TypeScript/issues/23812?

@vkrol I did see that, but I can drop decademoon's implementation in my codebase right now without waiting for releases of new features.

Another workaround that I'm using for now for tricky cases:

const restWithDefaults = { ...Component.defaultProps, ...rest };
return <Component {...restWithDefaults} />;

Nothing wrong with it I guess, so I'm leaving it here as a dirty yet simple workaround.

TS 3.2 and react 16.7 typings are fixing this. can we close ?

@Hotell how should it be handled eventually? I still can't get this working properly

To save others some time, here is a link to the release notes of Typescript 3:
Support for defaultProps in JSX

@cbergmiller I am afraid those are the release notes for TypeScript 3.1 🙃

still having the same issue with React.FunctionComponent

@denieler I wouldn't advise using defaultProps with React.FunctionComponent, it's not natural. It's better to use default function parameters:

interface HelloProps {
  name?: string;
  surname?: string;
}

const HelloComponent: React.FunctionComponent<HelloProps> = ({
  name = 'John',
  surname = 'Smith',
}) => {
  return <div>Hello, {name} {surname}!</div>
};

@mgol How would you define default function parameters if I didn't want to destructure the props?
I can only think of destructuring only the "defaulted" properties like so:

interface HelloProps {
  name?: string;
  surname?: string;
}

const HelloComponent: React.FunctionComponent<HelloProps> = ({
  name = 'John',
  surname = 'Smith',
  ...props
}) => {
  return <div>Hello, {name} {surname}! You are {props.age} years old.</div>
};

But I find it disgraceful to extract only some of the props.

@glecetre You can use:

HelloComponent.defaultProps = {
    name: 'John',
    surname: 'Smith'
}

@Glinkis please, note https://github.com/reactjs/rfcs/pull/107/files#diff-20b9b769068a185d90c23b58a2095a9dR184.

@glecetre Why do you not want to destructure all the props? It's simpler than defining defaultProps & easier to type. Class-based component's props type may bite you if you export to use externally as props that are required may not be required anymore if there's an entry for them in defaultProps. Using defaultProps also seems a little magical while in parameter destructuring it's all JavaScript.

Was this page helpful?
0 / 5 - 0 ratings