Matt Stow
Matt Stow

Reputation: 6389

How to specify (optional) default props with TypeScript for stateless, functional React components?

I'm trying to create a stateless React component with optional props and defaultProps in Typescript (for a React Native project). This is trivial with vanilla JS, but I'm stumped as to how to achieve it in TypeScript.

With the following code:

import React, { Component } from 'react';
import { Text } from 'react-native';

interface TestProps {
    title?: string,
    name?: string
}

const defaultProps: TestProps = {
    title: 'Mr',
    name: 'McGee'
}

const Test = (props = defaultProps) => (
    <Text>
        {props.title} {props.name}
    </Text>
);

export default Test;

Calling <Test title="Sir" name="Lancelot" /> renders "Sir Lancelot" as expected, but <Test /> results in nothing, when it should output "Mr McGee".

Any help is greatly appreciated.

Upvotes: 154

Views: 269184

Answers (10)

Mohammad Bilal
Mohammad Bilal

Reputation: 41

You can do it simply without dealing with Typescript Interfaces, just while defining the function that is going to accept those props using '?' operator as follows

const ChildComponent: React.FC<{ firstName: string, lastName?: string}> = ({ fName, lName }) => {

}

Calling child component with LastName

<ChildComponent firstName={"John"} lastName={"Doe"}/>  

Calling child component without LastName

<ChildComponent firstName={"John"}/>  

Upvotes: 1

rodrigocfd
rodrigocfd

Reputation: 8078

Update 2022

For function components, there is indeed a possible deprecation of defaultProps field. I don't believe this will happen so soon due to the sheer amount of code already written with it, but a warning being displayed in the console is very likely.

I'm using the solution below, which provides the correct behavior and proper TypeScript validation. It works with mixed defined/undefined properties, and also with properties with/without default values – that is, it covers all cases:

interface Props {
  name: string;
  surname?: string;
  age?: number;
}

const defaultProps = {
  surname: 'Doe',
};

function MyComponent(propsIn: Props) {
  const props = {...defaultProps, ...propsIn};

  return <div>{props.surname}</div>;
}

And VSCode autocomplete is spot on:

VSCode autocomplete

It has been tested with TypeScript 4.7.

Upvotes: 9

zhisme
zhisme

Reputation: 2800

as of react version 18.2 the following will do the trick.

Notice ? sign. The property can either have a value based on the type defined or its value can be undefined.

type TestProps {
  title?: string;
  name?: string;
}

export const Test = ({
  title = 'Mr', 
  name = 'McGee'
}: TestProps) => {
  return (
    <p>
      {title} {name}
    </p>
  )
}

Usage

<Test /> // Mc Gee
<Test title="Sir" name="Lancelot" /> // Sir Lancelot

Upvotes: 2

Barazu
Barazu

Reputation: 499

Here's how I like to do it:

type Props = { foo: Foo } & DefaultProps
type DefaultProps = Partial<typeof defaultProps>
const defaultProps = {
  title: 'Mr',
  name: 'McGee'
}

const Test = (props: Props) => {
  props = {...defaultProps, ...props}
  return (
    <Text>
      {props.title} {props.name}
    </Text>
  )
}

export default Test

Upvotes: 21

nort3x
nort3x

Reputation: 26

using @rodrigocfd answer i came up with this method,
it's useful in case inheritance is also involved (for instance i needed className being available in my components ,and not encapsulate them in div!)

// base.ts
export interface BaseProps {
  className?: string
}

export const basePropsDefault = {
  className: ''
}

export function setDefaultProps<T, P extends BaseProps>(t: T, defaultProps: P): T {
  // @ts-ignore
  t.defaultProps = {...basePropsDefault, ...defaultProps}
  return t
}


// Card.tsx
import {ReactElement} from "react";
import {setDefaultProps, BaseProps} from "../base";

export interface CardProps extends BaseProps {
  children: ReactElement
  internalPadding?: number,
}

const defaults = {
  internalPadding: 2,
} as CardProps

function Card(props: CardProps) {
  return (
    <>
      <div className={`shadow-lg container p-${props.internalPadding} ${props.className}`}>
        {props.children}
      </div>
    </>
  )
}

export default setDefaultProps(Card, defaults)

Upvotes: 1

Frank Simon
Frank Simon

Reputation: 111

I might be wrong, but passing the a default prop value on the function as the second voted reply says could lead to subtle bugs or over executed useEffects (I don't have enough rep to reply there, so here's a reproducible codesanbox)

Even if it's a really contrived example, and probably in most of the cases just bad component design, I have seen this more than once, even breaking full pages.

Upvotes: 0

fullStackChris
fullStackChris

Reputation: 1502

Adding my solution to the pot, I think it adds an additional level of readability and elegance onto the existing solutions.

Let's say you have a component MyComponent with a mix of required and optional props. We can separate these required and optional props into two interfaces, combining them for the full prop interface of the component, but only using the optional one to set the default props:

import * as React from "react";

// Required props
interface IMyComponentRequiredProps {
  title: string;
}

// Optional props
interface IMyComponentOptionalProps {
  color: string;
  fontSize: number;
}

// Combine required and optional props to build the full prop interface
interface IMyComponentProps
  extends IMyComponentRequiredProps,
    IMyComponentOptionalProps {}

// Use the optional prop interface to define the default props
const defaultProps: IMyComponentOptionalProps = {
  color: "red",
  fontSize: 40,
};

// Use the full props within the actual component
const MyComponent = (props: IMyComponentProps) => {
  const { title, color, fontSize } = props;
  return <h1 style={{ color, fontSize }}>{title}</h1>;
};

// Be sure to set the default props
MyComponent.defaultProps = defaultProps;

export default MyComponent;

Upvotes: 1

kDar
kDar

Reputation: 3436

I've found the easiest method is to use optional arguments. Note that defaultProps will eventually be deprecated on functional components.

Example:

interface TestProps {
    title?: string;
    name?: string;
}

const Test = ({title = 'Mr', name = 'McGee'}: TestProps) => {
    return (
        <p>
            {title} {name}
        </p>
    );
}

Upvotes: 118

Matt Stow
Matt Stow

Reputation: 6389

Here's a similar question with an answer: React with TypeScript - define defaultProps in stateless function

import React, { Component } from 'react';
import { Text } from 'react-native';

interface TestProps {
    title?: string,
    name?: string
}

const defaultProps: TestProps = {
    title: 'Mr',
    name: 'McGee'
}

const Test: React.SFC<TestProps> = (props) => (
    <Text>
        {props.title} {props.name}
    </Text>
);

Test.defaultProps = defaultProps;

export default Test;

Upvotes: 168

drewwyatt
drewwyatt

Reputation: 6027

To me, this doesn't look like a typescript issue.

DISCLAIMER: I have only tried this with typescript.

However, the problem is that props always exists (even as an empty object when nothing is passed in). There are 2 workaround for this, though.

The first, unfortunately, kills the super clean curly-brace-less syntax you have, but let's you keep defaultProps around.

interface TestProps {
    title?: string;
    name?: string;
}

const defaultProps: TestProps = {
    title: 'Mr',
    name: 'McGee'
}

const Test = (passedIn: TestProps) => {
    const props = Object.assign({}, defaultProps, passedIn);
    return (
        <p>
            {props.title} {props.name}
        </p>
    );
}

another alternative that might get a little hairy if you have a TON of props, but that lets you keep your original syntax is something like this:

const Test = (props: TestProps) => (
    <Text>
        {props.title || 'Mr'} {props.name || 'McGee'}
    </Text>
);

Hope this helps!

Upvotes: -5

Related Questions