Using TypeScript, how can I change interface based on the value of a property?

zeckdude

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',
};
jcalz

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.

Playground link to code

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

How i can return to my template a value based in a typescript decision?

How Can I change the value of a property in a component by a Vuex store action?

How can I create an object based on an interface file definition in TypeScript?

How can I change the switch display based on the value from the database

How can I change a readonly property in TypeScript?

How can I change a column value based on length of element in a DataFrame

How can I infer an object property's value in TypeScript when using mapped types?

How can I change the default font using in django admin interface?

How can I check that a string is a property a particular interface in TypeScript

How can I change the value of a property on the call of a reusable component on Reactjs?

How can I change the CSS property value to a value of an element?

How can I change the view output based on a value?

How can I dynamically change the value of a CSS property in React?

Is there a way to change whether an interface property is optional or not based on the value of another property(TypeScript)?

How can I change the value of an item in an array based on multiple conditions?

Typescript - How can I conditionally define a type based on another property

How can I change value from property in jQuery

How can I change the property value in subclass within swift?

How do I change the translateX() value of the transform property using JavaScript?

How can I change the value of a property in a list for just one value when the property value is ()?

How can I change the static property field's value in a property decorator in TypeScript?

How can I change the background color of a row based on the value using HTML, CSS and JavaScript?

Typescript: use either the one or other interface based on property value

How change object property value typescript?

How can I change the value of CSS custom property by 1 on button click using jQuery?

How can I duplicate a TypeScript interface but change the property type in the duplication?

How can I set default value of property based on theme in MAUI

How can I change ChartJS line color based on the value?

How to define the second property using the value of the first property in TypeScript interface