6

I'm a novice in Typescript and I'm trying to create React components with different variants, IE a Button with a 'foo' and an 'bar' variant.

I would like to use the component <Button variant="foo" /> for which I'd use an interface like so:

interface Props {
  variant: 'foo' | 'bar';
  prop1?: boolean;
  prop2?: boolean;
}

When creating a Button with the prop variant="foo" I want to allow certain props, and disable others. Same with the other variants. For this reason I created interfaces like so which is the result of many google / stackoverflow searches:

interface Props {
  variant?: never;
  prop1?: boolean;
  prop2?: boolean;
}

type Variant = 'foo' | 'bar' | 'baz';

interface DefaultVariant extends Omit<Props, 'variant'> {
  variant: Variant;
} 

interface FooVariant extends DefaultVariant {
  variant: 'foo';
  prop1?: never;
}

interface BarVariant extends DefaultVariant {
  variant: 'bar';
  prop1?: never;
  prop2?: never;
}

interface BazVariant extends DefaultVariant {
  variant: 'baz';
  prop1?: never;
  prop3: boolean;
}

...in order to use function overloads like so:

export default function Component(props: Props): ReactElement;
export default function Component(props: BazVariant): ReactElement;
export default function Component(props: BarVariant): ReactElement;
export default function Component(props: FooVariant): ReactElement;
export default function Component(props: Props | BarVariant | BazVariant | FooVariant): ReactElement {
  const ComponentVariant = variants[props.variant || 'foo']
  return (
    <ComponentVariant />
  )
}

const Foo = () => <div>Foo</div>

const Bar = () => <div>Bar</div>

const Baz = () => <div>Baz</div>



type Variants = {[key in Variant]: (props: any) => ReactElement };

const variants: Variants = {
  foo: Foo,
  bar: Bar,
  baz: Baz,
}

Hoping to use the component like so:

function App(): ReactElement {
  return (
    <>
      {/* should work: */}
      <Component /> 
      <Component prop1 prop2/>
      <Component variant="foo" prop2 />
      <Component variant="bar" />
      <Component variant="baz" prop2 prop3 />

      {/* should fail on prop1: */}
      {/* this works */}
      <Component variant="foo" prop1 prop2 />

      {/* should fail on presence of prop1 and prop2: */}
      {/* but fails on variant and prop1: */}
      {/* No overload matches this call.
        The last overload gave the following error.
        Type '"bar"' is not assignable to type '"foo"'.
        Type 'true' is not assignable to type 'undefined' */}
      <Component variant="bar" prop1 prop2 />

      {/* should fail on presence of prop1 and lack of prop3: */}
      {/* but fails on variant: */}
      <Component variant="baz" prop1 prop2 />
    </>
  )
}

And while this sort of works, in the sense that I cannot create Component without the correct props, the point of failure as determined by Typescript is not correct.

For example <Component variant="bar" prop1 prop2/> throws "No overload matches this call." as expected, but it erroneously states that 'bar' is not assignable to type 'foo', where the problem actually lies in the assignment of prop1 and prop2 that are not allowed in the BarVariant interface.

My problem, as far as I can tell, lies in the fact that Typescript always seems to infer the last overload, where I expected (and would like!) it to infer the overload related to the "variant" prop.

If anyone could share some light on what I'm doing wrong here it would be very much appreciated!

5
  • Reminds me of my question at stackoverflow.com/questions/64851834/…, could it be related? TS overloads based on order, could rearrange the order change it? Commented Nov 30, 2020 at 18:06
  • Thanks for your quick response. I tried to order the overloads from from most specific to least specific. Changing the order indeed changes the results. However it seems to only move the problem, not fix it. IE placing the BarVariant overload at the end makes typescript infer everything to that interface. Commented Nov 30, 2020 at 18:47
  • In that case, I think I'll have to sit this out and let someone come along who understands TS's type system better. Commented Nov 30, 2020 at 19:28
  • No problem, thank you for trying to help me! Commented Nov 30, 2020 at 19:31
  • This answer may help Commented Sep 28, 2022 at 6:16

1 Answer 1

1

I've ran into the same problem, here is my working example:

interface CommonProps {
    children: React.ReactNode;
}

interface TruncatedProps extends CommonProps {
    truncate?: 'on';
    expanded?: boolean;
}

interface NoTruncatedProps extends CommonProps {
    truncate?: 'off';
}

interface TextProps extends CommonProps {
    truncate?: 'on' | 'off';
    expanded?: boolean;
}

function Text(props: NoTruncatedProps): JSX.Element;
function Text(props: TruncatedProps): JSX.Element;

function Text(props: TextProps) {
    const { children, truncate, expanded } = props;
    const className = truncate ? 'u-text-truncate' : '';

    return (
        <div className={className} aria-expanded={!!expanded}>
            {children}
        </div>
    );
}

But the issue is that I can't make use spread operator for props like so:

...
const textProps: TextProps= {
        truncate: 'on',
        expanded: true,
        children: 'Text'
    };
<Text {...textProps} /> <- Typescript warning
...

You should explicitly point out the right type for textProps: TruncatedProps or NoTruncatedPops, which isn't handy.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.