I frequently need to define a type object where a property key is only accepted if another property/properties of the type are a certain value.
A simple example (in the context of React but should be applicable in any situation) is I need a type Button
object which accepts the following properties:
type Button = {
size: 'small' | 'large';
appearance: 'solid' | 'outline' | 'minimal';
isDisabled?: boolean;
hasFancyOutline?: boolean;
}
Now, I actually don't want the type to accept hasFancyOutline
if appearance
is not outline
and isDisabled
is false
.
The correct way is to do this is:
type SharedButtonProps = {
size: 'small' | 'large';
}
type NonOutlineButtonProps = SharedButtonProps & {
appearance: solid' | 'minimal';
isDisabled?: boolean;
}
type OutlineButtonProps = SharedButtonProps & {
appearance: 'outline';
isDisabled: false;
hasFancyOutline?: boolean;
}
type Button = NonOutlineButtonProps | OutlineButtonProps
I want to write a shorthand utility type called ConditionalProps
that intelligently does this for me. Something like this:
type Button = ConditionalProps<
{
size: 'small' | 'large';
appearance: 'solid' | 'outline' | 'minimal';
isDisabled?: boolean;
},
{
appearance: 'outline';
isDisabled: false;
hasFancyOutline?: boolean;
}
>
I'm thinking in pseudo-code, it'd work something like this:
type ConditionalProps<BaseProps, ConditionalProps> = {
// 1. Find keys with the same name in BaseProps & ConditionalProps. Optional and non-optional types such as `isDisabled?` and `isDisabled` need to be matched.
type MatchingProps = Match<BaseProps, ConditionalProps> // { appearance: 'solid' | 'outline' | 'minimal', isDisabled?: boolean }
type SharedProps = Omit<BaseProps, MatchingProps> // { size: 'small' | 'large' }
// 2. Find what's the values of the props if they don't match the condition, e.g. 'appearance' would be either 'solid' or 'minimal'
type FailConditionProps = RemainingValues<MatchingProps, ConditionalProps> // { appearance: 'solid' | 'minimal'; isDisabled?: boolean; }
// 3. Assemble
type FailConditionPlusSharedProps = SharedProps & FailConditionProps
type PassConditionPlusSharedProps = SharedProps & ConditionalProps
return FailConditionPlusSharedProps | PassConditionPlusSharedProps
}
EDIT
Titian's answer below is the exact solution for this. But I'm wondering if there's a way to rewrite ConditionalProps
to be even better.
I find myself writing a lot of types that are conditional on values it is given.
So for example,
type Button = {
size: 'small' | 'large';
isReallyBig?: boolean;
appearance: 'solid' | 'outline' | 'minimal';
hasFancyOutline?: boolean;
outlineBackgroundColor: string;
isDisabled?: boolean;
isLoading?: boolean;
}
Say I want to do:
isReallyBig?
is accepted only ifsize = 'large'
hasFancyOutline?
&outlineBackgroundColor
is accepted only ifappearance = ‘outline’
&isDisabled = false
isLoading
can betrue
only ifisDisabled = true
.
If I wanted to re-write ConditionalProps
to cleanly define this type, how would I do so? I was thinking implementation would be something like:
type Button = ConditionalProps<
{
size: 'small' | 'large';
appearance: 'solid' | 'outline' | 'minimal';
outlineBackgroundColor: string;
isDisabled?: boolean;
},
[
[
{ size: 'large' },
{ isReallyBig?: boolean }
], [
{ appearance: 'outline', isDisabled: false },
{ hasFancyOutline?: boolean }
], [
{ isDisabled: true },
{ isLoading?: boolean }
]
]
>
Is something like this achievable, or is there a better way of dealing with this scenario?