1

I have this kind of interface

export interface IButton {
 label: string;
 withIcon?: boolean;
 underlined?: boolean;
 selected?: boolean;
 iconName?: string;
 isLink?: boolean;
 href?: string;
 onCLick?: () => void;
}

Is it possible to make conditionally the use of iconName based on the use of withIcon ?

To make an example:

<Button label='test' /> ---> this should not throw error

<Button withIcon /> this should throw an error that alert me that iconName is missing.

Legeo
  • 784
  • 4
  • 20
  • 44

2 Answers2

3

You can achieve this by creating Union type of icon options as follows:

type IconOptions = { withIcon?: false; iconName?: string; } | { withIcon: true; iconName: string; }

What this means is that when withIcon is true, iconName will be required, otherwise it is not required.

You can then append this type as an intersection to IButton which will also need to be a type:

export type IButton = {
 label: string;
 underlined?: boolean;
 selected?: boolean;
 isLink?: boolean;
 href?: string;
 onCLick?: () => void;
} & IconOptions;

Playground link.

Ovidijus Parsiunas
  • 2,512
  • 2
  • 8
  • 18
  • 1
    This still allows for `{iconName:"foo", withIcon:false}`. A union of `{ withIcon : false }|{ withIcon: true; iconName: string; }` would be a better model (unless I misunderstood something). – spender Jun 12 '22 at 14:17
  • I wrote this answer based on the type that was provided on the example question. That's what I actually had it as initially, but I'm not sure how OP prefers it. – Ovidijus Parsiunas Jun 12 '22 at 14:20
  • On a second thought, OP did set it to optional because they didn't want it to always be there, so it makes sense for ```iconName``` not to be there when ```withIcon``` does not exist or is false. – Ovidijus Parsiunas Jun 12 '22 at 14:22
  • There is a problem: if i try to use type IconOptions = { withIcon?: false; } | { withIcon: true; iconName: string; }, typescript throw me an error in the component. (parameter) iconName: any Property 'iconName' does not exist on type 'PropsWithChildren>' – Legeo Jun 12 '22 at 14:24
  • Can you try changing ```IconOptions``` to the following and see if it fixes your issue: ```type IconOptions = { withIcon?: false; iconName?: string } | { withIcon: true; iconName: string; }``` – Ovidijus Parsiunas Jun 12 '22 at 14:26
  • Yup, it fixes in this way. I think that without iconName?: string into the first type typescript goes in rage mode because iconName doesn't exists from its point of view. – Legeo Jun 12 '22 at 14:30
  • Good to hear it helped, I have updated my answer :) – Ovidijus Parsiunas Jun 12 '22 at 14:31
  • 1
    @Legeo With a union of `{ withIcon? : false }|{ withIcon: true; iconName: string; }`, you have a [discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) (the discriminator is the `withIcon` prop). If you test the discriminator, the type will be appropriately narrowed and you will consequently be able to use the `iconName` property. So, for instance `const iconName = props.withIcon ? props.iconName : undefined` will work without inciting TypeScript's "rage mode" – spender Jun 13 '22 at 12:05
1

This seems to be similar to this quesion.

You can solve it by creating a union of both possible cases.

export type IButton = {
 label: string;
 underlined?: boolean;
 selected?: boolean;
 isLink?: boolean;
 href?: string;
 onCLick?: () => void;
} & ({
 iconName: string;
 withIcon: true
} | {
 iconName?: never;
 withIcon?: never | false
})

This will throw an error if one property is used without the other.

function main(){
  return (
    <>
      <Button label="abc"></Button> // valid
      <Button label="abc" iconName="abc" withIcon></Button> // valid
      <Button label="abc" withIcon></Button> // error: Property 'iconName' is missing in type
      <Button label="abc" iconName="abc"></Button> // error: Property 'iconName' is missing in type
      <Button label="abc" withIcon={false} iconName="abc"></Button> // error: Types of property 'iconName' are incompatible
    </>
  )
}

Playground

Tobias S.
  • 21,159
  • 4
  • 27
  • 45