DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 963,274 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Default Props in React/TypeScript
Adam Nathaniel Davis
Adam Nathaniel Davis

Posted on • Updated on

Default Props in React/TypeScript

[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 in this article, please feel free to call out my stooopidity in the comments.]

I just ran into something very... odd. It's one of those programming moments that makes you stop and say, "Wait... It can't really be like this, right???" It has to do with implementing default values for component props in React/TypeScript.

The Setup

Our team just began a brand new, "green fields" project. It will be written in React. (Great! That's my specialty.) Specifically, it will use TypeScript & React. (Umm... OK. I got some learnin' to do.) I've been wanting to get my feet wet in a TS project for awhile. So I've been diving in eagerly. But in the last week-or-so, something really threw me for a loop.

To illustrate the issue, I'm gonna take something from a plain-ol' JS component, and convert it into a TS component. The stub of my JS component looks like this:

export default function MyJSComponent(props) {
   return (
      <>
         Here is MyJSComponent:<br/>
         {props.children}
      </>
   );
}

MyComponent.propTypes = {
   requiredString: PropTypes.string.isRequired,
   requiredNumber: PropTypes.number.isRequired,
   optionalBoolean: PropTypes.bool,
   optionalString: PropTypes.string,
   optionalNumber: PropTypes.number,
};

MyComponent.defaultProps = {
   optionalBoolean: true,
   optionalString: 'yo',
   optionalNumber: 42,
};
Enter fullscreen mode Exit fullscreen mode

Nothing fancy here. A dead-simple component that accepts up to 5 props, with 2 of those props being required. For the 3 optional props, default values are assigned. If the component is wrapping other content, that content will be rendered with props.children. This is basically React 101.

So let's set about converting this to TypeScript. In TS, we can infer data types right in the function signature. And in TS, just as in JS, we can supply default values for optional parameters - right in the function signature. So that could look something like this:

export default function MyTSComponent(
   requiredString: string,
   requiredNumber: number,
   optionalBoolean: boolean = true,
   optionalString: string = 'yo',
   optionalNumber: number = 42,
) {
   return (
      <>
         Here is MyComponent:<br/>
         {props.children}
      </>
   );
}
Enter fullscreen mode Exit fullscreen mode

Except... that doesn't work, does it? This fails on two key levels:

  1. When React invokes a component, it doesn't supply the props to the components as an array of arguments. It supplies them in a single object - the props object. So TS will complain about the above code because it will realize that the props object does not correlate with the requiredString type of string.

  2. The above code obliterates the standard React convention of being able to call props.children. We haven't defined any of the arguments as props, and therefore, there is no props.children to render.

In other words, the approach above works great when we're writing a "regular" TS function. But it won't work for a TS/React component. We'll need to account for the fact that all of the props are being passed into the component as a single object.

One approach is to alter your tsconfig.json to disable strict mode and allow implicit any types. That would look like this:

export default function MyTSComponent(props) {
   return (
      <>
         Here is MyComponent:<br/>
         {props.children}
      </>
   );
}
Enter fullscreen mode Exit fullscreen mode

With all of your configs disabled/relaxed, you can actually get the above code to run/compile. But if your solution to TS problems is to disable the power of TS, then... don't use TS.

And if your answer to coding problems in any language is to turn off strict mode or to relax the core config constructs... Well, let's just say that nothing in this article - or this entire site - is going to help you in any way.

Assuming that you're not in favor of disabling TS's core strengths, the next step is to figure out how to get TS to "accept" that props object. In other words, we need to explicitly define what's in props.

Inline Type-Hinting

I believe that, in TS, whenever possible, it's best if you can define your data types right in the function signature. It's efficient. It's easy for other devs to "grok". So now that we know that we must specifically define the props object being passed in, maybe we can do this?

export default function MyTSComponent(props: {
   requiredString: string, 
   requiredNumber: number, 
   optionalBoolean: boolean = true, 
   optionalString: string = 'yo',
   optionalNumber: number = 42,
   children: JSX.Element,
}) {
   return (
      <>
         Here is MyComponent:<br/>
         {props.children}
      </>
   );
}
Enter fullscreen mode Exit fullscreen mode

Except... that doesn't work, does it? If you try typing this out in your IDE, you'll notice that it does, for the most part, work - until you reach the point where you're trying to define default values on the optional properties. (Also, even if the default values worked, the idea of having to manually define props.children is just... yuck.)

Interfaces

It seems to me that interfaces are the "default" TypeScript way to handle these kinda situations. With a good interface, you can definitively type all the values that are expected in React's traditional props object. After tinkering with many different configurations, this is what I came up with:

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

export default function MyTSComponent({
   requiredString,
   requiredNumber,
   optionalBoolean = true,
   optionalString = 'yo',
   optionalNumber = 42,
   children,
}: Props) {
   return (
      <>
         Here is MyComponent:<br/>
         {children}
      </>
   );
}
Enter fullscreen mode Exit fullscreen mode

Unlike the other attempts shown above, this one actually works. React knows which values are required and which ones are optional. TypeScript understands the type associated with each argument. But IMHO, this approach still has... problems.

  1. The full list of properties is spelled out twice - once in the interface, and once in the function signature. This is necessary because, if we neglect to list, say, requiredString in the interface, then TS won't know what type to assign to it. And if we neglect to list requiredString in the function signature, it simply won't be available anywhere within the function.

  2. We have to list children in the function signature. For a long-time React guy, that just feels... wrong. It would be like having to define the console.log() method before you can use it. In React, children is supposed to be something that you just get "for free".

  3. Speaking of React conventions, object destructuring obliterates the near-universal React practice of referencing props.foo or props.children. That may not be a big deal to some. But for me, it's huge. When I'm combing through the logic in a component, I definitely want to have a clear indicator that a particular variable was passed into the component as a prop. Once you destructure the props out of their original object, you lose that clear scoping.

defaultProps

You may be thinking, "If you want default prop values, why don't you just use the built-in functionality for defaultProps??" I certainly investigated this. It would look like this:

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

const defaultProps: Props = {
   requiredString: '',
   requiredNumber: 0,
   optionalBoolean: true,
   optionalString: 'default',
   optionalNumber: 42,
}

const MyTSComponent: React.FC<Props> = (props) => {
   console.log(props);
   return (
      <>
         Here is MyComponent:<br/>
         {props.children}
      </>
   );
};

MyTSComponent.defaultProps = defaultProps;

export default MyTSComponent;
Enter fullscreen mode Exit fullscreen mode

There's a lot to like here. It maintains the traditional props convention. It doesn't require explicitly defining props.children. It makes for a clean function signature.

One thing I don't like about this approach is that I couldn't seem to get it to work unless I also defined default values inside defaultProps for the required props. If I remove requiredString and requiredNumber from the defaultProps definition, TS complains about it. Still - that's not really that big of a deal.

So is this the end of the article? The "real" solution for default props in React/TS? Umm... no.

No sooner did I start researching this pattern than I found out that there's a big push to deprecate defaultProps on functional components.

Given the issues I've outlined above, I don't honestly understand why anyone would want to deprecate defaultProps on functional components. They say things like, "default values are already handled in the function signature". Umm... no, they're not (at least not in a way that properly accommodates React's props object).

Regardless of the twisted reasoning behind this, it does seem as though this deprecation might happen. So with a big sigh, I moved on to search for other solutions.

Alt Text

My WTF?!?! Moment

Honestly, at this point, I started getting pretty annoyed. What I'm trying to do is, in React/JS, a five-minute lesson. When you first start doing React with plain-ol' JavaScript, it takes mere minutes to realize how you can set default values on the optional props. And yet, in React/TS, this seemingly-simple operation requires jumping through a ridiculous number of hoops. How can this be???

Imagine you travel to another country - one that speaks a language very similar to your own. While you're there, you say to your tour guide, "In your language, how do I say 'thank you'?" And the tour guide points you to a dozen different web pages that all explain ways that you can try to say 'thank you' - with no definitive answer. Finally, the tour guide says, "Well, in our variant of the language, there's really no simple way to say 'thank you'."

What???

It's not like I'm trying to migrate from JavaScript to Objective-C, or from JavaScript to C++. I'm merely moving from React/JS to React/TS. And I'm trying to do something that really should be drop-dead simple. And yet... I'm burning many many hours trying to solve this most-basic of questions.

Nevertheless, I pushed onward. The fact that this "problem" feels ridiculous to me doesn't do anything to help me solve the problem.

In-Function Processing

At this point, I started to think of "other" ways that I could provide default values. So I looked at applying them inside of the function itself. That looks like 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}
      </>
   );
}
Enter fullscreen mode Exit fullscreen mode

This doesn't throw any TS linting errors. However, it won't run, because React complains that the props object is not extensible. So, to get around that, we can do a deep clone of props with a cloneObject() function that I outlined in one of my previous articles.

[Yeah, yeah - I get it. Cloning props just so I can manually add default values feels a bit... hack-ish. But I'm just outlining a progression-of-thought here.]

So with an extra line to clone the props object, the code looks like this:

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

export default function MyTSComponent(props: Props) {
   props = cloneObject(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}
      </>
   );
}
Enter fullscreen mode Exit fullscreen mode

This approach... works. It compiles. It preserves the conventional props object, along with props.children. And for about a day-or-two, I really thought that this was the answer.

Then I started noticing a few annoyances...

While the above code indeed "works" just fine, I found that things started to get wonky when I began adding functions inside the functional component. Consider this example:

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

export default function MyTSComponent(props: Props) {
   props = cloneObject(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);

   const getLetterArrayFromOptionalString = (): Array<string> => {
      return props.optionalString.split(''); 
   };

   return (
      <>
         Here is MyComponent:<br/>
         {props.children}
      </>
   );
}
Enter fullscreen mode Exit fullscreen mode

I've set a default value of 'yo' on props.optionalString. And inside getLetterArrayFromOptionalString(), I'm trying to split() that string into an array of letters. But TS won't compile this. It complains that the props.optionalString object is possibly undefined - even though I clearly defined a default value at the top of the function.

Why does it do this?? Well, TS sees the function as being bound at the point when the component is mounted. And at the point that the component is mounted, there has been no default value set for props.optionalString yet. It doesn't matter that getLetterArrayFromOptionalString() will never be called until after a default value has been added to props.optionalString. TS doesn't fully grok that.

TS chokes on this because the split() function requires a type string | RexExp. But props.optionalString is type: string | undefined.

Where did that | undefined come from in our props.optionalString type? It was dynamically added by TS because the optionalString parameter is defined as optional (i.e., with the ? appended to it).

When you add ? to an interface property, TS will append | undefined as part of the type definition. This may seem like a good thing, but it can cause headaches later on because TS will expect you to write a whole bunch of code that's tolerant of undefined values - even though you know that you manually set a value for the variable, and it will never be undefined.

Annnnnd... I'm right back to the drawing board.


Alt Text

Finally - A Solution

For the time being, I think I have a working solution. (Until I find some other edge case where everything gets borked up...) It looks like this:

//all.props.requires.ts
export type AllPropsRequired<Object> = {
   [Property in keyof Object]-?: Object[Property];
};

// my.ts.component.tsx
interface Props extends PropsWithChildren<any>{
   requiredString: string,
   requiredNumber: number,
   optionalBoolean?: boolean,
   optionalString?: string,
   optionalNumber?: number,
}

export default function MyTSComponent(props: Props) {
   const args: AllPropsRequired<Props> = {
      ...props,
      optionalBoolean: props.optionalBoolean !== undefined ? props.optionalBoolean : true,
      optionalString: props.optionalString !== undefined ? props.optionalString : 'yo',
      optionalNumber: props.optionalNumber !== undefined ? props.optionalNumber : 42,
   };
   console.log(args);

   const getLetterArrayFromOptionalString = (): Array<string> => {
      return args.optionalString.split('');
   };

   return (
      <>
         Here is MyComponent:<br/>
         {props.children}
      </>
   );
}
Enter fullscreen mode Exit fullscreen mode

So what's actually happening here?

The first thing you see is the AllPropsRequired type. In TS, it's something called a partial. I'm not gonna go through a tutorial on that here. Suffice it to say that AllPropsRequired is a type that will make all the properties of some other generic interface required. That will be important in a second...

The Props interface is fairly "standard" - nothing too magical there.

Inside MyTSComponent, the first thing I'm doing is creating a new object, based on the props, cast to the type AllPropsRequired. In other words, in the args object, I'm stripping out that optional ? indicator on every one of the properties.

I'm doing this because every property either had a value passed in (if it was required), or it will have a default value added to it. So none of the properties should be undefined, and we don't want the property's type to reflect that it could possibly be undefined.

Inside the args definition, the first thing I do is spread the ...props object. I do this so I can avoid manually spelling out each one of the required properties in the object. I only want to spell out the optional properties, and spreading ...props allows me to do that.

Then, for each of the optional properties, I'm checking to see if something was passed in. If nothing was passed in (i.e., if the property is undefined), I set the value to its default.

This approach preserves my props.children feature - because I've done nothing to alter/destroy the original props object. But throughout the component, any other time when I want to reference props, I'll use the args object.

This code compiles, and the line:

return args.optionalString.split('');
Enter fullscreen mode Exit fullscreen mode

runs just fine. It doesn't throw any errors because, in the args object, optionalString doesn't have a type of string | undefined. It simply has a type of string.


Alt Text

It Shouldn't Be This Hard

Maybe I'm missing something here. Maybe, in the next week or two, I'll realize how silly this whole little journey was. Someone in the comments will say something like, "Why didn't you just use setDefaultProps()?" And I'll feel really silly for having burned several days trying to reinvent the wheel.

But I know that I'm not entirely alone in this. If you google around for things like "typescript default props functional components", you'll find a number of articles and Stack Overflow questions that (attempt to) address this same problem. And they all run into the same limitations. It just kinda feels like an... oversight to me.

And don't even get me started on the push to deprecate defaultProps for functional components. That just feels ridiculous to me. Or maybe it's not - I dunno. It could just be that something's not "clicking" right in my brain...

[NOTE: A few days after this was posted, I came up with an improved/revised method. That's highlighted in part two of this series...]

Top comments (35)

Collapse
 
jwp profile image
John Peters • Edited on

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 Author

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
 
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 Author • Edited on

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 Author

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

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 Author

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 Author

Haha - I see your point.

Collapse
 
7iomka profile image
7iomka • Edited on

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 Author

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 on

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 Author

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 on

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 Author

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 Author

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 Author

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 Author

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
 
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
 
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 Author

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
 
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 Author

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
 
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
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
bytebodger profile image
Adam Nathaniel Davis Author

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
 
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/...

"I made 10x faster JSON.stringify() functions, even type safe"

☝️ Must read for JS devs