Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mutually exclusive props in a React Component

How do I make sure my functions components props are mutually exclusive, with code completion?

My code:

type Variant =  'a' | 'p' | 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'button'

interface TypographyProps {
    children: React.ReactNode | string
    className?: string | string[]
    variant?: Variant
}

type Text = TypographyProps
type Primary = TypographyProps & { primary: true }
type Secondary = TypographyProps & { secondary: true }
type Extra = TypographyProps & { extra: true }
type Dark = TypographyProps & { dark: true }
type Muted = TypographyProps & { muted: true }

function Typography(props: Text): JSX.Element;
function Typography(props: Primary): JSX.Element;
function Typography(props: Secondary): JSX.Element;
function Typography(props: Extra): JSX.Element;
function Typography(props: Dark): JSX.Element;
function Typography(props: Muted): JSX.Element;
function Typography({ children, className, variant, ...props }: TypographyProps): JSX.Element {

}

In WebStorm, when using control+spacebar (code completion), I don't get hints for props like primary or secondary. When I use more than one prop, I always get the error the first prop doesn't exist on type Muted.

e.g.

<Typography primary secondary>Foo</Typography>
like image 503
CherryNerd Avatar asked May 06 '26 12:05

CherryNerd


1 Answers

Just want to extend @jcalz's answer for general usage:

type MutuallyExclude<T, E extends keyof T> =
  | {
      [K in E]: { [P in K]: T[P] } & Omit<T, E> & {
          [P in Exclude<E, K>]?: never;
        } extends infer O
        ? { [P in keyof O]: O[P] }
        : never;
    }[E]
  | ({ [K in E]?: never } & Omit<T, E>);

And the usage would like:

interface TypographyProps {
  primary: true;
  secondary: true;
}
type ExclusiveProps = 'primary' | 'secondary';
const Typography = (props: MutuallyExclude<TypographyProps, ExclusiveProps>) => {
  ...
};

const Case1 = () => <Typography primary />; // fine
const Case2 = () => <Typography />; // fine
const Case3 = () => <Typography primary secondary />; // error

While the error message is hard to be understand. I will update the answer if I figure out a better solution, or maybe it will be complement in the comment.

If one of the properties must be assigned, the simplest generic type would be:

type MutuallyExclude<T, E extends keyof T> = {
  [K in E]: { [P in K]: T[P] } & {
    [P in Exclude<E, K>]?: never;
  } & Omit<T, E>;
}[E];

The usage is as same as above, but the second case will return error:

interface TypographyProps {
  primary: true;
  secondary: true;
}
type ExclusiveProps = 'primary' | 'secondary';
const Typography = (props: MutuallyExclude<TypographyProps, ExclusiveProps>) => {
  ...
};

const Case1 = () => <Typography primary />; // fine
const Case2 = () => <Typography />; // error
const Case3 = () => <Typography primary secondary />; // error

In my case, it is used for customized RadioGroup since I want it can be described by either options or children. While it would be pretty ambiguous when both of them are assigned.

like image 181
Heng Lee Avatar answered May 09 '26 02:05

Heng Lee



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!