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,
}
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
)})
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
}
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,
}
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();
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
)}
)
}
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>
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.
Top comments (0)