Default Props in React/TS - Part Deux
Tags: react, typescript, javascript, tutorial
Published: 2020-06-21 17:33:07

A few days ago, I posted a long article about my struggle to find a solution for setting default prop values in React/TS components.  Based on feedback in the comments, I toyed around with several other approaches, but I ultimately settled (for now) on a revised approach to the "solution" in that article.  I'll outline that here.
<br/>

## A Quick Recap

I'm a long-time React/JS dev (and even longer with other JS frameworks, going back to the advent of jQuery).  For the first time, I'm working on a team where we're spinning up a "green fields" React/TS project.  It's not like TS is completely foreign to me.  After all, I've done several years of C# dev.  But converting my "standard" JS knowledge into TS for the first time still requires a bit of acclimating.

Specifically, I want to be able to create React/TS components that fit the following parameters (parameters that were _extremely easy_ to implement in React/TS):
<br/>

1. I'm creating functional components (as opposed to class-based components).<br/>

1. Those functional components must be able to accept a single object containing all the properties (i.e., `props`) that were passed into the component.  This is _standard_ React functionality.<br/>

1. I must be able to annotate the types associated with each `prop` value.  (This is also _standard_ React functionality, but it should obviously fit quite nicely into TypeScript.)<br/>

1. I must be able to designate some `props` as required - while others may be optional.  (Again, pretty standard stuff in both React/JS & React/TS.)<br/>

1. For any `prop` that is optional, I need the ability to designate a default value for that `prop`, if none is supplied when the component is invoked.<br/>

1. Inside the body of the functional component, I wanna be able to reference any of the `props` values in a single object.  In React/JS, these are often referenced as `props.foo` or `props.bar`.  But I wouldn't mind if the name of that object is something else, such as `args` or `params` or whatever.<br/>

1. I don't wanna use any solutions that are in imminent danger of being deprecated.  (This is why I'm not using the native `defaultProps` feature that currently ships with React.  There's a lot of chatter about _removing_ this feature for functional components.)<br/>

1. BONUS: I'd really prefer to _not_ have to manually define `props.children` - only because, in React/JS, this is never necessary.  In React/JS, `props.children` is just sorta "there" - for free.<br/>

This may feel like a big pile of requirements.  But most of them are "requirements" that were pretty _standard_ or _easy to achieve_ before I switched from React/JS to React/TS.
<br/>

## My Previous "Solution"

A few days ago, this was my working solution:

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

First, a big shout-out to @chico1992 for pointing out that my custom partial `AllPropsRequired<>` is only recreating what TS already provides with `Required<>`.  So I've washed that out of my solution.

Second, that same commenter also gave me some useful working code to look at other ways to encapsulate the default values right into the function signature itself.  However, even with those (awesome) suggestions, I was still stuck with the idea of having to manually chunk the required/optional values into a new object, which I didn't really like.  

So I went back to the drawing board and came up with, what seems to me for now, to be a better solution.
<br/>

## Solution - Part Deux

In my first solution above, there's some verbose, clunky verbiage designed to set the default value on any optional `prop` that wasn't provided.  It's the section that looks like this:

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

That's not the worst chunk of code that I've ever spit out, but it's definitely not very "clean".  So I got to thinking:
<br/>

> What if I could create a `setDefaults()` function that would auto-magically do a lot of that stuff for me?

<br/><br/>

<br/>That led me to create the following universal helper function:

```typescript
// set.defaults.ts
export default function setDefaults<Props, Defaults>(props: Props, defaults: Defaults): Required<Props> {
   let newProps: Required<Props> = {...props} as Required<Props>;
   const defaultKeys = Object.keys(defaults) as (string)[];
   defaultKeys.forEach(key => {
      const propKey = key as keyof Props;
      const defaultKey = key as keyof Defaults;
      Object.defineProperty(newProps, key, {
         value: props[propKey] !== undefined ? props[propKey] : defaults[defaultKey],
      });
   });
   return newProps;
}
```

Some of you TS pros may see some other opportunities for optimization there.  So I'm not claiming that `setDefaults()` is in its final form.  But this one function does some nice things for me.

It accepts the existing `props` and a second, generic object that gives the definition for any prop keys that should have a default value.  It then uses generics to return a `props` object that adheres to whatever type was originally defined.

And here's what the revised code looks where `setDefaults()` is now used:

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

export const MyTSComponent: FC<Props> = (props: Props) => {
   const args = setDefaults(props, {
      optionalBoolean: true,
      optionalString: 'yo',
      optionalNumber: 42,
   });
   console.log(args);

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

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

Obviously, if you don't have any optional `props`, or if you don't want any default values to be set on those `props`, then you never need to call `setDefaults()` inside the function at all.

If you _do_ have optional `props` that require default values, it's now done with code that's as just as simple/efficient as the native `defaultProps` feature.  

In fact, I personally like this approach _better_, because when you use `defaultProps`, those default values end up getting set somewhere else in the file in a way that is not always easy to "grok" when you're reading through the code.  With this approach, I'm not setting the default values _in_ the function signature, but they reside _right underneath it_.  So they should be easy to spot when simply reading the code.

I've also switched to using `React.FC` as the type for the functional component.  When using this type, and setting the interface to extend `PropsWithChildren<any>`, I don't have to define `props.children`.  It's there by default, on the `props` object.

This approach also solves the problem of the optional properties having a type like `string | undefined` or `number | undefined`.  That additional `| undefined` causes headaches with the TS compiler because it forces you to write code that's tolerant of `undefined` values - _even after you've set a default value on the `prop` and you know it will never be `undefined`_.
<br/>

## Conclusion

I still stand by the theme of my original rant in the prior article.  This shouldn't be _this_ hard.  This is extremely easy in React/JS.  But getting it to work in React/TS required a ridiculous amount of research.  Perhaps even more frustrating, it led to a number of confused shrugs when I tried to query longtime-TS devs about how to solve this.

One of the more annoying aspects of this journey was listening to the responses where TS devs told me stuff like, "You shouldn't worry about having all of your `props` in a single object."  I'm sorry, but having all of the `props` in a single object is a very _standard_ pattern that's outlined repeatedly _in React's core docs_.  The idea that I should just discard this convention because I'm switching to functional React/TS components is, well... _silly_.

Knowing myself, I'll probably throw out this solution in another month (or less).  But for the time being, this feels like the closest thing to an "answer".

Please feel free to point out anything that I've screwed up or overlooked!