I want to enforce a property being present on an object as long as a specific value is being used for another property.
In the following example, I want the backgroundSurfaceValue
to be required when the variant
value is ButtonVariant.Subtle
. I am trying to solve it using Distributive Conditional Types but I'm not doing something correctly. Any ideas what I'm missing?
Here's a link to the code in the TS playground.
enum ButtonVariant {
Primary = 'Primary',
Secondary = 'Secondary',
Subtle = 'Subtle'
}
type SubtleButtonVariant = ButtonVariant.Subtle
enum SurfaceValue {
Surface0 = 'Surface0',
Surface1 = 'Surface1',
Surface2 = 'Surface2'
}
type ButtonProps = SubtleButtonVariant extends ButtonVariant.Subtle
? {
variant: ButtonVariant;
backgroundSurfaceValue: SurfaceValue;
name: string;
}
: {
variant: ButtonVariant;
backgroundSurfaceValue: never;
name: string;
};
const subtleButton: ButtonProps = {
variant: ButtonVariant.Subtle,
backgroundSurfaceValue: SurfaceValue.Surface1,
name: 'Subtle Button',
};
const primaryButton: ButtonProps = {
variant: ButtonVariant.Primary,
name: 'Primary Button',
};
You want a ButtonProps
object to have a variant
property, whose literal type can be used to discriminate which of several shapes the whole object has. That indicates you really want ButtonProps
to be a discriminated union type, as follows:
type ButtonProps = {
variant: ButtonVariant.Subtle;
backgroundSurfaceValue: SurfaceValue;
name: string;
} | {
variant: Exclude<ButtonVariant, ButtonVariant.Subtle>;
backgroundSurfaceValue?: never;
name: string;
};
If the variant
prop is ButtonVariant.Subtle
, then backgroundSurfaceValue
is required and must be of type SurfaceValue
.
Otherwise, if the variant
prop is either ButtonVariant.Primary
or ButtonVariant.Secondary
(which is represented above as Exclude<ButtonVariant, ButtonVariant.Subtle>
via the Exclude
utility type), then backgroundSurfaceValue
is essentially prohibited, being an optional property of the impossible never
type, meaning the only value you could possibly read from it is undefined
.
Let's test that out:
const subtleButton: ButtonProps = {
variant: ButtonVariant.Subtle,
backgroundSurfaceValue: SurfaceValue.Surface1,
name: 'Subtle Button',
};
const primaryButton: ButtonProps = {
variant: ButtonVariant.Primary,
name: 'Primary Button',
};
const badButton: ButtonProps = { // error!
// ~~~~~~~~~ <-- Property 'backgroundSurfaceValue' is missing
variant: ButtonVariant.Subtle,
name: 'Bad Button'
}
const alsoBadButton: ButtonProps = { // error!
// ~~~~~~~~~~~~~ <-- Type 'SurfaceValue' is not assignable to type 'undefined'
variant: ButtonVariant.Secondary,
backgroundSurfaceValue: SurfaceValue.Surface2
name: 'Also Bad Button'
}
Looks good.
Note that your version had several problems which prevented it from working as desired:
It wasn't actually a distributive conditional type because the checked type SubtleButtonVariant
is not a generic type parameter; it's a specific type, equivalent to ButtonVariant.Subtle
. That's just a regular conditional type, which will evaluate to the true branch alone (since SubtleButtonVariant extends ButtonVariant.Subtle
is always true).
For it to be distributive, you'd have needed to write something like
type ButtonProps<T extends ButtonVariant> = T extends ButtonVariant.Subtle
? {
variant: ButtonVariant;
backgroundSurfaceValue: SurfaceValue;
name: string;
} : {
variant: ButtonVariant;
backgroundSurfaceValue: never;
name: string;
};
but that wouldn't be a type you could just assign to a variable without specifying the input type parameter. The whole point of distributive conditional types is to distribute some type operation across unions in their inputs. So you need to give it a union.
You could have fixed that by either specifying it in another type
type BProps = ButtonProps<ButtonVariant>;
or by having T
default to ButtonVariant
:
type ButtonProps<T extends ButtonVariant = ButtonVariant> =
T extends ButtonVariant.Subtle ? {
variant: ButtonVariant;
backgroundSurfaceValue: SurfaceValue;
name: string;
} : {
variant: ButtonVariant;
backgroundSurfaceValue: never;
name: string;
};
Either way would produce a new union type:
/* type ButtonProps = {
variant: ButtonVariant;
backgroundSurfaceValue: SurfaceValue;
name: string;
} | {
variant: ButtonVariant;
backgroundSurfaceValue: never;
name: string;
}*/
But of course, that still doesn't work... the variant
property is the same on both halves of the union, so there's nothing to discriminate. Also, the backgroundSurfaceValue
property on one of the union members is a required property of type never
, which is impossible. If we fix both of those we get:
type ButtonProps = {
variant: ButtonVariant.Subtle;
backgroundSurfaceValue: SurfaceValue;
name: string;
} | {
variant: Exclude<ButtonVariant, ButtonVariant.Subtle>;
backgroundSurfaceValue?: never;
name: string;
};
That is, you get a discriminated union again. There might be some reason why you'd want to use a generic conditional type for ButtonProps
, but it's not obvious from the example. For the question as asked, you should just use a discriminated union instead of a conditional type, distributive or otherwise.
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments