DEV Community

Cover image for Default Props in React/TypeScript

Default Props in React/TypeScript

Adam Nathaniel Davis on June 19, 2020

[DISCLAIMER: My dev experience is quite substantial, but I just started doing TypeScript, oh... about 3 weeks ago. So if I've screwed something up...
Collapse
 
jwp profile image
John Peters • Edited

Adam,

From this.

interface Props extends PropsWithChildren<any>{
   requiredString: string,
   requiredNumber: number,
   optionalBoolean?: boolean,
   optionalString?: string,
   optionalNumber?: number,
}

export default function MyTSComponent(props: Props) {
   props.optionalBoolean = props.optionalBoolean !== undefined ? props.optionalBoolean : true;
   props.optionalString = props.optionalString !== undefined ? props.optionalString : 'yo';
   props.optionalNumber = props.optionalNumber !== undefined ? props.optionalNumber : 42;
   console.log(props);
   return (
      <>
         Here is MyComponent:<br/>
         {props.children}
      </>
   );
}

To this?

// props is still an interface but it has behaviors as a class
class Props extends PropsWithChildren<any>{
  constructor(props)
   requiredString: string = props.optionalString !== undefined ? props.optionalString : 'yo';
   requiredNumber: number =props.optionalNumber !== undefined ? props.optionalNumber : 42;
   optionalBoolean?: boolean =props.optionalBoolean !== undefined ? props.optionalBoolean : true;

}


export default function MyTSComponent(props: Props) {

   console.log(props);
   return (
      <>
         Here is MyComponent:<br/>
         {props.children}
      </>
   );
}

Here's two possible examples:

//  I think this may work.
export default function MyTSComponent(props: Props) {

// But, this always work
export default function MyTSComponent(props) {
  let properties : props = new Props(props);

There is another way to cast Javascript into TS types. Like this:

//but this bypasses the ctor logic
let newtype:Props = props;
// so we redesign the Props class.
// meaning always initilize with default values, except for optional props.
class Props extends PropsWithChildren<any>{ 
   requiredString: string =  'yo';
   requiredNumber: number  42;
   optionalBoolean?: boolean;  
}


A number of javascript folks don't like the new keyword, no problem create a factory...


export function CreatedProps(props){
  return new Props(props);

Collapse
 
jwp profile image
John Peters

Ok Adam, updated my response.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

When I type:

class Props extends PropsWithChildren<any> {

}

The TS linter tells me that:

'PropsWithChildren' only refers to a type, but is being used as a value here.

Collapse
 
jwp profile image
John Peters

Ok make that one a class too

Thread Thread
 
jwp profile image
John Peters

Or change extends to implements.

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Just for reference, I'm hoping you find a way around this, or someone posts a solution. I ran into similar things and I just can't be bothered to type stuff out multiple times, it's a waste of effort for the small benefits of type safety at compile time. (I use WebStorm, it can already autocomplete everything and JSDoc will already provide me types and indication of using the wrong ones while I code). That all said, I'm a dyed in the wool C# programmer and would love to be pulling across the great parts of that to the JS world with TypeScript. But yeah, not if I'm going to spend hours of my life trying to explain to a compiler my perfectly logical structure.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Oh, yeah - I'm definitely nodding along to everything you've written here. You probably have more C# experience than me, but I've done a good bit of it and enjoy it. I also use WebStorm and find that it does a really great job of tying most things together for me - and showing me when something seems out-of-place - without using TS.

And I love the description of "trying to explain to a compiler". That really sums up some of my frustrations here. If I'm writing "bad" code, or buggy code, then of course, I'd love for any tool to be able to point that out. But it's always frustrating if you've written something that you know works perfectly well - but the compiler/linter won't stop complaining about it.

Not sure if you read to the end of the article (and I totally understand if you didn't), but for the time being, I think that last "solution" is what I'm running with for now. It's actually not too much more verbose than using PropTypes.defaultProps. IMHO, it's still a little "ugly" - but not so much that it makes my dev eye start twitching.

Collapse
 
miketalbot profile image
Mike Talbot ⭐

I did make it to the end, but think I'd missed that you were so much closer in all of the "right!!" I was saying to the sections before haha.

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis

Haha - I see your point.

Collapse
 
craigkovatch profile image
Craig Kovatch

For a class component, the correct TS syntax for default props is e.g.:

  public static defaultProps: Pick<TextFieldInputProps, 'shouldSelectTextOnFocus' | 'text' | 'type'> = {
    shouldSelectTextOnFocus: true,
    text: '',
    type: 'text'
  };

This is how you avoid having to define default values for required props -- because then of course they aren't really required, are they? In fact the React TS typedefs know how to infer that a prop provided in a defaultProp structure implies that the prop is not actually required on the component:


// Any prop that has a default prop becomes optional, but its type is unchanged
// Undeclared default props are augmented into the resulting allowable attributes
// If declared props have indexed properties, ignore default props entirely as keyof gets widened
// Wrap in an outer-level conditional type to allow distribution over props that are unions
type Defaultize<P, D> = P extends any
    ? string extends keyof P ? P :
        & Pick<P, Exclude<keyof P, keyof D>>
        & Partial<Pick<P, Extract<keyof P, keyof D>>>
        & Partial<Pick<D, Exclude<keyof D, keyof P>>>
    : never;

type ReactManagedAttributes<C, P> = C extends { propTypes: infer T; defaultProps: infer D; }
    ? Defaultize<MergePropTypes<P, PropTypes.InferProps<T>>, D>
    : C extends { propTypes: infer T; }
        ? MergePropTypes<P, PropTypes.InferProps<T>>
        : C extends { defaultProps: infer D; }
            ? Defaultize<P, D>
            : P;

(FWIW I don't actually grok those typedefs, but I understand what they do.)

For a function component, if you assign defaultProps inline, TS seems to infer all the correct things, e.g.:

export const TextFieldWidget: React.FC<TextFieldProps> = props => { ... };
TextFieldWidget.defaultProps = { kind: 'outline' };
Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

Hi, Craig, and thanks for the feedback. Thank you for outlining the class-based approach. I suppose I shoulda mentioned in the article that, the reason all my examples use functional components, is because the decision was made for the project that we'd be using functional React components. But I appreciate you taking the time to put those examples here.

As for the last example you give, the one that deals with functional components, there's a whole section in this article that outlines that approach - and also explains why I did not choose to use it. Primarily, there's a significant fear that defaultProps will be deprecated for functional components.

So that's kinda what led to this whole article. There are these techniques for doing this in class-based components - but we're not going to switch everything over to class-based components over this one simple issue. Then there is this technique for doing this in a function-based component - but there's a ton of chatter that this will be deprecated - and I don't want to base the dev for a "green fields" project on something at significant risk of being deprecated. Subsequently, the search for alternate solutions becomes... ugly.

Collapse
 
craigkovatch profile image
Craig Kovatch

I'm with you :) I saw your post because I also think the removal of defaultProps on FCs is a mistake, and saw your link on the RFC comments

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis

Oh, cool! I'm glad you saw my comment on the RFC.

Collapse
 
7iomka profile image
7iomka • Edited

My workaround on this weird issue

import { useTranslation } from 'react-i18next';
import { SpriteIcon } from '@components/svg';

type Point = {
  icon: SpriteIcon;
  title: string;
  label: string;
};

type Props = {
  title?: string;
  desc?: string;
  items?: Point[];
};

const Points: React.FC<Props> = (props) => {
  const { t } = useTranslation();
  const defaultProps: Props = {
    title: t('points.title'),
    desc: t('points.desc'),
    items: [
      {
        icon: 'Req', // autocomplete suggestions works fine (FINALLY!)
        title: '150,000+',
        label: t('points.item1'),
      },
      {
        icon: 'Flyer',
        title: '7,400,000+',
        label: t('points.item2'),
      },
      {
        icon: 'Inventory',
        title: '1,000+',
        label: t('points.item3'),
      },
      {
        icon: 'Deal',
        title: '90%',
        label: t('points.item4'),
      },
    ],
  };
  const cProps = { ...defaultProps, props };
  const { title, desc, items } = cProps;
  ...

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

I see what you did there. By simply spreading ...defaultProps first, those default values will be overridden by any real values that were passed in.

But doesn't this leave the optional properties still defined with type string | undefined?? That was one of the big problems that I was trying to solve. If a prop has a default value, it should never be undefined. And I should never have to write code that accounts for this possibility.

Collapse
 
franksvalli profile image
David Calhoun • Edited

I was wrestling with this a lot until I found this pretty sane and simple approach from typescript-cheatsheets/react:

type GreetProps = {
  age: number;
  name?: string;
} & typeof defaultProps;

const defaultProps = {
  name: 'Joe',
};

const Greet = (props: GreetProps) => {
  /*...*/
};
Greet.defaultProps = defaultProps;
Enter fullscreen mode Exit fullscreen mode

This seems like it satisfies everything except your defaultProps concern, which seems like it's a really premature concern. The React 17 RC has just been released, and defaultProps are still here, and I'm struggling to find references to discussions about it being deprecated soon. If there were plans to deprecate it, I think we'd see something in the docs, or the usage signature would change to Greet.UNSAFE_defaultProps in advance, as is the team's habit with things like UNSAFE_componentWillMount.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

FWIW, this is the open RFC thread that discusses (amongst several things) deprecating defaultProps on functional components.

github.com/reactjs/rfcs/pull/107

To be clear, it's obviously just an RFC and it may never come to pass. And I typically pay little-or-no attention to RFCs unless they become adopted and implemented. Because you can't run your programming life based on what might change in the language.

But in this case, I'd already read references to this in multiple other places. And because I was just getting into TS, I thought, "Well, if there's even a minor chance that it goes away - how do I accomplish this without the feature?" And that started my descent down the rabbit hole.

It's entirely possible that, in several years, I'll be shaking my head over the time I wasted on this, because the RFC was never adopted and defaultProps are still readily available. I hope that's the case.

Collapse
 
chico1992 profile image
chico1992 • Edited

Hi Adam the closest I came to a solution that could satisfy your need is following and everything is nicely typecheck

interface Props {
    requiredString: string;
    requiredNumber: number;
    optionalBoolean?: boolean;
    optionalString?: string;
    optionalNumber?: number;
}

export const MyTSComponent: React.FC<Props> = ({
    optionalString = "default",
    optionalBoolean = true,
    optionalNumber = 42,
    ...props
}) => {
    console.log(props);
    optionalString.split("");
    return (
        <>
            Here is MyComponent:
            <br />
            {props.children}
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

with this solution you could put the destructured props inside an args object inside your component

export const MyTSComponent: React.FC<Props> = ({
    optionalString = "default",
    optionalBoolean = true,
    optionalNumber = 42,
    ...props
}) => {
    const args = { optionalString , optionalBoolean , optionalNumber};
    console.log(props);
    args.optionalString.split("");
    return (
        <>
            Here is MyComponent:
            <br />
            {props.children}
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

And typescript has a Required type that does the same as your AllPropsRequired type

hope this helps

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

First, thank you for showing me the Required type! I had no idea that existed. Makes a lot more sense than doing it manually with my own custom partial.

Second, I do like your approach. The only thing I find lacking in it, is the need to manually chunk those values into an args object (assuming you want them in a single object - like I do). But that's not really a huge objection, is it? Hmm...

From looking at your example, one of the problems with my prior approaches was probably that I wasn't consistently leveraging React.FC<>. From many of the examples I've been looking at online, it's not at all clear that this should be used whenever creating a functional React component - but I'm starting to think that's the case.

Very cool - thanks!!

Collapse
 
chico1992 profile image
chico1992

Your welcome

The nice thing about the React.FC is that it defines the return type of your function and it add children?: React.ReactNode to your props so no need to handle that prop yourself

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis

OIC. I think I had kinda stumbled into a different way to handle that. Cuz in my (final) example, my interface is defined as:

interface Props extends PropsWithChildren<any> {...}
Enter fullscreen mode Exit fullscreen mode

But I think I like the React.FC way better.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Another "challenge" with the approach you've outlined here is that the requiredString and requiredNumber values only exist under the props object - but optionalString, optionalBoolean, and optionalNumber exist as standalone variables. As you've pointed out, you can get them back into one object, but you'd have to manually add requiredString and requiredNumber to that object as well.

That's not insurmountable, but I gotta play with it for a bit to see if there's a slicker way of handling that...

Collapse
 
chico1992 profile image
chico1992

You could try something around these lines

export const MyTSComponent: React.FC<Props> = ({
    optionalString = "default",
    optionalBoolean = true,
    optionalNumber = 42,
    ...args
}) => {
    const props = { optionalString , optionalBoolean , optionalNumber, ...args};
    console.log(props);
    props.optionalString.split("");
    return (
        <>
            Here is MyComponent:
            <br />
            {props.children}
        </>
    );
};

whit this you would get a fully typed props object that you could use as you used to

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis

I've actually built a helper function now that takes the existing props and a simple object that defines any default values and then returns it mapped to the Props type. I'll probably outline that in a near-future post.

Thanks for taking the time to point me along!

Collapse
 
clindseismic profile image
Carl Lind • Edited

As of Nov 2022, Typescript 4.9 has a 'satifies' operator which solves this in a cleaner way, check out the stack overflow here: stackoverflow.com/questions/757665...

const defaultProps = {
  foo: 'hello',
  bar: 'world',
}

const Component: FC<Props> = (props) => {
  const propsWithDefaults = {
    ...defaultProps,
    ...props,
  }

  const { foo, bar } = propsWithDefaults;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
nahuel profile image
Nahuel Greco

Check this:

import {ComponentProps, FC} from "react"

const Button : FC<{ requiredString: string, 
                      optionalNum?: number }> = 
 (props: ComponentProps<typeof Button>) => {

  props = { optionalNum : 42,
            ... props}
 // now use props.requiredString and props.optionalNum
Enter fullscreen mode Exit fullscreen mode

Also check github.com/microsoft/TypeScript/is...

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
bytebodger profile image
Adam Nathaniel Davis

There's an entire section in my article that covers defaultProps. I specifically outline there why I chose to forgo that approach - because the current indication is that defaultProps will be deprecated on functional components.

Collapse
 
qpwo profile image
Luke Harold Miles

For future readers, I think this is the best way. I wrote a short post about it dev.to/qpwo/closest-thing-to-struc...

function Car(props: {
    model: string
    owner: [string, string]
    year?: number
    make?: string
}): JSX.Element {
    props = {
        ...{
            year: 1999,
            make: "toyota",
        },
        ...props
    }
    return <div>
        {props.owner[0]} {props.owner[1]}{"'s"} {props.year} {props.make} {props.model} goes vroom.
    </div>
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
igorpavlichenko profile image
Igor Pavlichenko

Hi Adam.
What about the exclamation mark typescript operator?

I'm using it in combination with MyFunctionalComponent.defaultProps whenever the compiler is complaining that the prop might be null or undefined.. but I'm sure you could combine it with one of your solutions.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Honestly, I've gotta spend some more time looking into that exclamation operator. I'm new enough to TS that I'll freely admit not being aware of it. I've been travelling a bit and not able to sit down and test this in my IDE. I'm not sure that it addresses the issue - but it's definitely intriguing.

Thank you for bringing this to my attention!!

Collapse
 
sick profile image
Jannaee Sick

Well, well, well. I'm just starting to learn React and use Typescript for work and am doing a little research on Proptypes and I came across this article in the vast sea of articles. I didn't realize it was you. What a surprise! Thanks for this write up.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Howdy, Jannaee! Nice to see that my random angry rants are finding people in the ether. Hope all is going well with you. My sis now lives in your region. (OK, not incredibly close - but, Asheville.) Good to hear from you!

Collapse
 
nickdivona profile image
Nick DiVona

Just wanted to say this post really helped me out. Much appreciated.

Collapse
 
kainbacher profile image
Roland Kainbacher

Why not make it simple and readable?

vuejs.org/guide/typescript/overvie...

Collapse
 
ntt2k profile image
Trung Nguyen

Hi @adam ,

I glad I'm not the only one have problems with this.
But have you tried this approach? -->
github.com/typescript-cheatsheets/...