Reputation: 448
I have a fairly complex React component within a Typescript based application. There are multiple groups of props that should be required only when another prop is present/true. Is there some way to accomplish this in Typescript? I haven't gotten in too much advanced typing with it yet.
I made some attempts with discriminated unions but I don't think it quite covers all the possibilities as it's possible that any combination or none of these additional groups of props will be required.
This is what I'm working with here.
interface CommonProps {
...a bunch of props...
}
interface ManagedProps extends CommonProps {
managed: true;
...props for only when managed is present/true...
}
interface ServerSideProps extends CommonProps {
serverSide: true;
...props for only when serverSide is present/true...
}
interface Props = ???
The baseline is that CommonProps
would apply to this component. If managed
is true
then ManagedProps
should also apply. If serverSide
is true
then ServerSideProps
also applies.
I don't have this situation quite yet, but it would be nice if the optional groups of props could override a prop defined in the CommonProps
.
Upvotes: 3
Views: 1033
Reputation: 2667
I have had great success using ts-toolbelt's Union.Strict
helper.
First, let's define your two separate sets of possible types, combine them in a strict union and write a type guard that tells our React component which set of props we're using. I'll extend the example you used in your OP:
import { Union } from 'ts-toolbelt';
interface CommonProps {
commonProp: string
}
interface ManagedProps extends CommonProps {
managed: true;
managedBoolean: boolean;
managedNumber: number;
}
interface ServerSideProps extends CommonProps {
serverSide: true;
serverBoolean: boolean;
serverNumber: number;
}
const isManaged = (props: Props): props is ManagedProps => {
return props.managed;
}
type Props = Union.Strict<ManagedProps | ServerSideProps>
Now, let's write a React component that leverages the type guard we wrote:
const OurComponent: React.FC<Props> = (props) => {
if (isManaged(props)) {
return <div>Managed {props.commonProp}</div>
} else {
return <div>ServerSide {props.commonProp}</div>
}
}
This will cause errors if you use the wrong types. You can check that out in this StackBlitz I created.
Upvotes: 2
Reputation: 32166
You could use conditional types to get something like this. For example:
interface CommonProps {
foo: string;
}
interface ManagedProps extends CommonProps {
managed: true;
managedOnlyProp: boolean;
}
interface ServerSideProps extends CommonProps {
serverSide: true;
serverOnlyProp: boolean;
}
type MappedProps<T> =
T extends { managed: true } ? ManagedProps :
T extends { serverSide: true } ? ServerSideProps :
CommonProps;
// Example function to test this:
declare function takeProps<T>(props: T & MappedProps<T>): void;
// And:
// This is OK, neither flag is present, so CommonProps are used:
takeProps({ foo: "" })
// This is an error. The managed flag is present, so we must also include the managed only props:
takeProps({ foo: "", managed: true }) // Gives error: Property 'managedOnlyProp' is missing in type '{ foo: string; managed: true; }' but required in type 'ManagedProps'.
// But this is okay, the flag is present but set to false...
takeProps({ foo: "", managed: false })
// And likewise for the serverSide versions:
takeProps({ foo: "", serverSide: true }) // Gives error: Property 'serverOnlyProp' is missing in type '{ foo: string; serverSide: true; }' but required in type 'ServerSideProps'.
takeProps({ foo: "", serverSide: true, serverOnlyProp: false }) // OK, no errors
takeProps({ foo: "", serverSide: false }) // OK, no errors
Here's a Playground Link.
Note there is a caveat in that this only works for each flag individually. When used in combination, you can still pass an invalid object:
// This is wrong (missing serverOnlyProp) but no error is produced... :(
takeProps({foo: "", serverSide: true, managed: true, managedOnlyProp: false})
Upvotes: 1