Skip to main content

Typing defaultProps

You May Not Need defaultProps

As per this tweet, defaultProps will eventually be deprecated. You can check the discussions here:

The consensus is to use object default values.

Function Components:

type GreetProps = { age?: number };

const Greet = ({ age = 21 }: GreetProps) => // etc

Class Components:

type GreetProps = {
age?: number;
};

class Greet extends React.Component<GreetProps> {
render() {
const { age = 21 } = this.props;
/*...*/
}
}

let el = <Greet age={3} />;

Typing defaultProps

Type inference improved greatly for defaultProps in TypeScript 3.0+, although some edge cases are still problematic.

Function Components

// using typeof as a shortcut; note that it hoists!
// you can also declare the type of DefaultProps if you choose
// e.g. https://github.com/typescript-cheatsheets/react/issues/415#issuecomment-841223219
type GreetProps = { age: number } & typeof defaultProps;

const defaultProps = {
age: 21,
};

const Greet = (props: GreetProps) => {
// etc
};
Greet.defaultProps = defaultProps;

See this in TS Playground

For Class components, there are a couple ways to do it (including using the Pick utility type) but the recommendation is to "reverse" the props definition:

type GreetProps = typeof Greet.defaultProps & {
age: number;
};

class Greet extends React.Component<GreetProps> {
static defaultProps = {
age: 21,
};
/*...*/
}

// Type-checks! No type assertions needed!
let el = <Greet age={3} />;
React.JSX.LibraryManagedAttributes nuance for library authors

The above implementations work fine for App creators, but sometimes you want to be able to export GreetProps so that others can consume it. The problem here is that the way GreetProps is defined, age is a required prop when it isn't because of defaultProps.

The insight to have here is that GreetProps is the internal contract for your component, not the external, consumer facing contract. You could create a separate type specifically for export, or you could make use of the React.JSX.LibraryManagedAttributes utility:

// internal contract, should not be exported out
type GreetProps = {
age: number;
};

class Greet extends Component<GreetProps> {
static defaultProps = { age: 21 };
}

// external contract
export type ApparentGreetProps = React.JSX.LibraryManagedAttributes<
typeof Greet,
GreetProps
>;

This will work properly, although hovering overApparentGreetPropsmay be a little intimidating. You can reduce this boilerplate with theComponentProps utility detailed below.

Consuming Props of a Component with defaultProps

A component with defaultProps may seem to have some required props that actually aren't.

Problem Statement

Here's what you want to do:

interface IProps {
name: string;
}
const defaultProps = {
age: 25,
};
const GreetComponent = ({ name, age }: IProps & typeof defaultProps) => (
<div>{`Hello, my name is ${name}, ${age}`}</div>
);
GreetComponent.defaultProps = defaultProps;

const TestComponent = (props: React.ComponentProps<typeof GreetComponent>) => {
return <h1 />;
};

// Property 'age' is missing in type '{ name: string; }' but required in type '{ age: number; }'
const el = <TestComponent name="foo" />;

Solution

Define a utility that applies React.JSX.LibraryManagedAttributes:

type ComponentProps<T> = T extends
| React.ComponentType<infer P>
| React.Component<infer P>
? React.JSX.LibraryManagedAttributes<T, P>
: never;

const TestComponent = (props: ComponentProps<typeof GreetComponent>) => {
return <h1 />;
};

// No error
const el = <TestComponent name="foo" />;

See this in TS Playground

Misc Discussions and Knowledge

Why does React.FC break defaultProps?
TypeScript 2.9 and earlier

For TypeScript 2.9 and earlier, there's more than one way to do it, but this is the best advice we've yet seen:

type Props = Required<typeof MyComponent.defaultProps> & {
/* additional props here */
};

export class MyComponent extends React.Component<Props> {
static defaultProps = {
foo: "foo",
};
}

Our former recommendation used the Partial type feature in TypeScript, which means that the current interface will fulfill a partial version on the wrapped interface. In that way we can extend defaultProps without any changes in the types!

interface IMyComponentProps {
firstProp?: string;
secondProp: IPerson[];
}

export class MyComponent extends React.Component<IMyComponentProps> {
public static defaultProps: Partial<IMyComponentProps> = {
firstProp: "default",
};
}

The problem with this approach is it causes complex issues with the type inference working with React.JSX.LibraryManagedAttributes. Basically it causes the compiler to think that when creating a JSX expression with that component, that all of its props are optional.

See commentary by @ferdaber here and here.

Something to add? File an issue.