Conditional Properties for React TypeScript Functional Components
Tags: typescript, react, codequality
Published: 2021-04-23 16:00:00

## Pattern Description
A property or properties should only be present when another property has a specific value.

## Example Problem
For example: Say you want three possible actions on a component, download, preview, and print, and you want to have buttons who's click events execute those actions. The actions are grouped as follows, the component will either allow the user to preview and print a PDF OR to download a PDF. 

You could make the methods optional and qualify for them at run time, but this defeats the purpose of TypeScript. Something like:

```TypeScript
interface ActionComponent {
    className:string,
    ... // other properties go here
    purpose:"print" | "download",
    onDownload?:()=>void,
    onPreview?:()=>void,
    onPrint?:()=>void,
}
```

And then in your code you can wire up events to these with something  like ...

```TypeScript
 return (
     {props.purpose === "download" && ( 
         <button onClick={props.onDownload!}>
         </button>
     )}
     {props.purpose === "print" && (
         // render print buttons wired to with props.onPreview and props.Print 
     )})
```

Here, we're using ! to force TypeScript to compile with the optional ``props.onDownload`` method, we'll have to do the same for the print buttons, and we're assuming that the properties will be populated. In our parent component we can set the purpose property to "download" and not populate the onDownload property resulting in exactly the type of runtime error TypeScript is designed to avoid. There are other approaches that will also cause avoidable problems, such as using a ternary operator to qualify if `props.onDownload` is populated and handling its absence at runtime, again defeating the purpose of using TypeScript.

## Solution
With TypeScript we can create conditional properties using custom types and discriminating unions. Create an interface with the common properties for the component

```TypeScript
interface BaseProps {
    className:string,
    ... // other properties go here
}
```

And now create a type from a discriminating union, I'll explain how that works as we go along. 

```TypeScript
type PdfButtonProps = 
| {
    purpose: "download",
    onDownload:()=>void,
} | {
    purpose: "print",
    onPreview:()=>void,
    onPrint:()=>void,
}
```

The type of PdfButtonProps is determined by the discriminating union between the two types. The discrimination occurs on the shared property, which is `purpose`. You could think of it in terms of a ternary operator, and it equates to something like this:
```TypeScript
const pdfButton = purpose === "download" ? new PdfDownloadButton() : new PdfPrintButtons();
```

When we declare our functional component we can create a new type as an intersection of our BaseProps interface and our PdfButtonProps type, and use that as our functional component props (change this to suit your preferred approach to declaring functional components).

```TypeScript
type PdfComponentProps = BaseProps & PdfButtonProps;

const PdfComponent: React.FC<PdfComponentProps> = (props) => {
    ...
    return (
        ...// other possible components
        {props.purpose === "download" && (
            // render download button wired with props.onDownload
        )}
        {props.purpose === "print" && (
            // render print buttons wired with props methods
        )}
    )
}
```

In parent component's code:

```TypeScript
<div>
    <PdfComponent 
        className="form-buttons-pdf"
        purpose="download"
        onDownload={onDownloadHandler} /> // Compiles!
    
    <PdfComponent
        className="form-buttons-pdf"
        purpose="download"
        onPreview={onPreviewHandler}
        onPrint={onPrintHandler} /> // Does not compile
</div> 
```
The first instance compiles, but the reason the second instance of PdfComponent does not compile is because the type of PdfButtonProps with `purpose === "download"` does not have an onPreview or onPrint property, and because the code doesn't provide for the onDownload property. If the first instance's purpose was set to "print" it wouldn't compile as there is no onDownload property for that type, and the onPrint and onPreview properties have not been provided.

### Further reading
[TypeScript Conditional Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) 

[TypeScript Union and Intersections](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html)