Defining interdependent props in Typescript for React Components
using Discriminated Unions.
This is not something new or fancy. Just writing because it’s not a common pattern for a lot of React Components.
Why?
For example, let’s say you have a date picker component, react-calendar, which provides you the functionality of doing both, selecting just a date or a date-range, which is controlled by the selectRange
prop.
We can define the props of Calendar like this:
Here, I am defining the ValueType
to be either a single Date or a tuple of two Dates (narrowing the definition constraint that it will have length two, instead of using Date[]). This works fine when I pass a single date in the value
as props but typescript starts telling me that I haven’t handled the tuple case in the onChange
function.
In older versions of Typescript (< 2.6) you could fix this error by defining the type of value in your onChange
function. But this is a callback function, not a utility function that your code calls. So you should not be defining what parameters your function accepts but rather the Component should be defining what parameters will be provided. So the fix that you were applying was not a fix but a hack. This was allowed in older versions of Typescript because not a lot of libraries were typed and you needed function bi-variance. Read more about that here.
Now that the community has typed and covered a whole lot of cases we can get stricter with this and enable strictFunctionalTypes
. This flag ensures a callback functions are checked against the parameters being passed by the caller. Which leads us to this error.
This can be fixed with a hack, which I would not recommend as you are literally bypassing Typescript’s checker.
Solution
Explicitly define the relation between objects using a technique called Discriminated Union.
Solution 1:
Solution 2:
Both are nearly identical.
Usage within Component
Here is usage example on how to narrow down types so that Typescript does not throw errors and check your code correctly.
Caveats
So there is one gotcha that you have to take care of, Typescript does not narrow down type of related props if you destructure this.props
.
This is because inferred types do not propagate back to the source. Source.
So that is all I have to share. Go ahead type your components, increase Typescript adoption. Don’t write hacks like `value as Date`.
Further Reading
- Typescript 2.6 Release Notes
- swyx’s react-typescript-cheatsheet
- Discriminated Unions at Typescript Deep Dive
- “Understanding TypeScript’s Structural Type System” by Drew Colthorp: