Default Props in React/TypeScript
Tags: react, typescript, javascript, tutorial
Published: 2020-06-19 17:09:07

[**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.
<br/>

## 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:

```javascript
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,
};
```

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:

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

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

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

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:

```typescript
export default function MyTSComponent(props) {
   return (
      <>
         Here is MyComponent:<br/>
         {props.children}
      </>
   );
}
```

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

## 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?

```typescript
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}
      </>
   );
}
```

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_.)
<br/>

## 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:

```typescript
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}
      </>
   );
}
```

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

1. 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".<br/>

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

## 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:

```typescript
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;
```

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/81yrzd26jc3s4l3qmgl2.jpg)

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

## 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:

```typescript
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}
      </>
   );
}
```

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:

```typescript
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}
      </>
   );
}
```
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:

```typescript
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}
      </>
   );
}
```

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ra2h6wr6ztq4qu89onnd.jpg)

## 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:

```typescript
//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}
      </>
   );
}
```

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: 

```typescript
return args.optionalString.split('');
```

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/01tbhtgkiitltttojb87.jpg)

## 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...]