SolvedDefinitelyTyped Implementing defaultProps with ts 2.0 strict null checks

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?

44 Answers

✔️Accepted Answer

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

Other Answers:

+1
I am currently battling this issue.

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

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> = {...}.

@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>
};

More Issues: