DEV Community

loading...

Conditional Properties for React TypeScript Functional Components

Phil Smith
Senior developer working mostly with JavaScript, Typescript and C#, and very occasionally Go.
・3 min read

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:

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

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

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

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

interface BaseProps {
    className:string,
    ... // other properties go here
}
Enter fullscreen mode Exit fullscreen mode

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

type PdfButtonProps = 
| {
    purpose: "download",
    onDownload:()=>void,
} | {
    purpose: "print",
    onPreview:()=>void,
    onPrint:()=>void,
}
Enter fullscreen mode Exit fullscreen mode

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:

const pdfButton = purpose === "download" ? new PdfDownloadButton() : new PdfPrintButtons();
Enter fullscreen mode Exit fullscreen mode

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

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
        )}
    )
}
Enter fullscreen mode Exit fullscreen mode

In parent component's code:

<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> 
Enter fullscreen mode Exit fullscreen mode

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

TypeScript Union and Intersections

Discussion (0)