When working with union types, generally it's easiest to just make an ADT with a tagged union. However, sometimes this isn't possible.
A representative case is React Router's <Route/>
component. Here, there are three optional props: component
, render
, and children
, but there must be 1 and exactly 1 of these 3 passed.
For simplicity I've pared down the types here, but the currently published types look sort of like this:
interface Route {
render?: string
component?: string
children?: string
someOtherProps?: any
}
They should really be more like this:
interface BetterRouteRender extends Route {
render: string
component?: undefined
children?: undefined
}
interface BetterRouteComponent extends Route {
render?: undefined
component: string
chldren?: undefined
}
interface BetterRouteChildren extends Route {
render?: undefined
component?: undefined
Children: string
}
type BetterRoute =
| BetterRouteRender
| BetterRouteComponent
| BetterRouteChildren
const invalidRouteBecauseEmpty: BetterRoute = {}
const invalidRouteBecauseDupe: BetterRoute = { render: 'render', component: 'component' }
const validRoute: BetterRoute = { render: 'render' }
So, two questions:
- Is there a better way of handling the above?
- If not, I am looking for help trying to write a type like this:
type BetterOptionalTypeConstructor<T, KS extends Array<keyof T>> = unknown
...which would be used something like BetterOptionalTypeConstructor<Route, ['render', 'component', 'children']>
, and would spit out the BetterRoute
type shown above.
I haven't put much effort into this yet, but it seems like Typescript doesn't currently support "mapped union types", as far as I can tell. Any insight on this front would be appreciated!