Material-ui: Typescript: in ListItem, property button is no more boolean

Created on 20 Mar 2019  ·  31Comments  ·  Source: mui-org/material-ui

The following code in not compiling anymore in mui v4.0:

<ListItem button={editable} >
  <ListItemText primary="text" />
</ListItem>

where editable is a boolean value.

  • [x] This is not a v0.x issue.
  • [x] I have searched the issues of this repository and believe that this is not a duplicate.

Expected Behavior 🤔

Should compile without error.

Current Behavior 😯

Type 'boolean' is not assignable to type 'true'.

button={editable}

../../../node_modules/@material-ui/core/ListItem/ListItem.d.ts:23:38
23 ExtendButtonBase>;

The expected type comes from property 'button' which is declared here on type 'IntrinsicAttributes & { action?: ((actions: ButtonBaseActions) => void) | undefined; buttonRef?: ((instance: any) => void) | RefObject | null | undefined; centerRipple?: boolean | undefined; ... 6 more ...; TouchRippleProps?: Partial<...> | undefined; } & { ...; } & { ...; } & CommonProps<...> & Pick<...>'

Your Environment 🌎

| Tech | Version |
|--------------|---------|
| Material-UI | 4.0.0-alpha.4 |
| React | 16.8.4 |
| @types/react | 16.8.8 |
| TypeScript | 3.3.3333 |

bug 🐛 List typescript

Most helpful comment

I have the same issue. I would like to do <ListItem button={false} />, but I can't now because, if specificed, boolean must be true, which doesn't make sense for me.

Types should not contain the default value. I think the good way to do is to replace { button?: false } by { button?: boolean }, and in the definition of ListItem fallback button to false when undefined and li and fallback to true when undefined and div.

All 31 comments

I fail to reproduce the problem. Do you have a reproduction?

Thank you @oliviertassinari for looking at it.

I cannot reproduce on CodeSandbox, even if I use the same configuration as home. :disappointed_relieved:

I made a minimal project where the compilation fails because of this issue (see its README in order to see how to use it):
https://github.com/sveyret/issue-mui-14971

@sveyret Please add a lockfile to your repository. Otherwise changed dependencies over time might change to result.

@eps1lon Done.

@sveyret Looks like a limitation of TypeScript. For more details see #15049. Will keep an eye on this one but you might have to use an any cast

Thank you @eps1lon, I hadn't notice it became a discriminent. I'll update my code for that.

We have added a test which includes a workaround (with runtime overhead) for this issue. If anybody can come up with a solution that breaks the test in #15049 but passes all other tests we would be happy to accept PRs with that patch.

I have the same issue with MeunItem component...

Just wanted to add that if you provide ref prop to ListItem then typescript requires that you specify button={true}. It doesn't accept false and it doesn't allow you to omit button prop. So the workaround from this test https://github.com/mui-org/material-ui/pull/15049 doesn't work here.

The workaround I'm using is basically hiding ref prop from the compiler:

// Only store `ref` here
const restProps: any = {
  ref: myRef,
};

return (
  <ListItem button={false} {...restProps}>
    {content}
  </ListItem>
);

Another way would be to drop typechecking for ListItem entirely, but it's a bit worse imo.

const ListItemComponent = ListItem as any;

<ListItemComponent button={false} ref={myRef}>
  {content}
</ListItemComponent>

Would be happy to know if there is a better workaround.

Just wanted to add that if you provide ref prop to ListItem then typescript requires that you specify button={true}

What type is that ref?

Just wanted to add that if you provide ref prop to ListItem then typescript requires that you specify button={true}

What type is that ref?

React.RefObject<HTMLDivElement>

That you encounter an error is expected though. It's just not a helpful error message:

The component used for the root node. Either a string to use a DOM element or a component. By default, it's a li when button is false and a div when button is true.

-- https://material-ui.com/api/list-item/#listitem-api

For button={false} you will get a ref to an HTMLLIElement. With button={true} you will get HTMLDivElement.

However, it might not be possible by falling back to a more abstract type. If you use more abstract refs I recommend casting to any. For specific ref types our types should catch errors (without a helpful message though).

@eps1lon got it, no error after specifying correct ref type. Thank you! My apology for hijacking this thread, my problem clearly had nothing to do with this issue.

Is there a fix or for the initial issue from @sveyret ? :)
Thanks!

/related #16245

Reproduction/broken test pr #16315

Just wanted to add that if you provide ref prop to ListItem then typescript requires that you specify button={true}. It doesn't accept false and it doesn't allow you to omit button prop. So the workaround from this test #15049 doesn't work here.

The workaround I'm using is basically hiding ref prop from the compiler:

// Only store `ref` here
const restProps: any = {
  ref: myRef,
};

return (
  <ListItem button={false} {...restProps}>
    {content}
  </ListItem>
);

Another way would be to drop typechecking for ListItem entirely, but it's a bit worse imo.

const ListItemComponent = ListItem as any;

<ListItemComponent button={false} ref={myRef}>
  {content}
</ListItemComponent>

Would be happy to know if there is a better workaround.

What's the point of having this button as required and it's value can only be true🤦‍♂️

In case it got drowned out: The latest status is https://github.com/mui-org/material-ui/issues/14971#issuecomment-477029285.

Probably a silly question -
From the type definition of ListItem, I see this:

declare const ListItem: OverridableComponent<ListItemTypeMap<{ button?: false }, 'li'>> &
  ExtendButtonBase<ListItemTypeMap<{ button: true }, 'div'>>;

I wonder why we are explicitly putting false here as the type?

I wonder why we are explicitly putting false here as the type?

  • false | undefined is the discriminator for when a ListItem does not use Button
  • true for when it does use a Button

See http://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions

I have the same issue. I would like to do <ListItem button={false} />, but I can't now because, if specificed, boolean must be true, which doesn't make sense for me.

Types should not contain the default value. I think the good way to do is to replace { button?: false } by { button?: boolean }, and in the definition of ListItem fallback button to false when undefined and li and fallback to true when undefined and div.

In my case, this particular issue causes errors with ListItem's "component" prop as well. If I manually edit the { button: true } to { button: boolean } and { button?: false } to { button?: boolean }, then the "component" prop is getting suddenly available on ListItem.

Having the same issue, I'm trying to do

    const unlocked = status !== "locked";
    return(<ListItem button={unlocked} />)

and I get the error message Type 'boolean' is not assignable to type 'true'..

Seems pretty counter-intuitive to me

Getting a similar problem when using styled-components like so:

const StyledListItem = styled(ListItem)`
    &:hover {
        background-color: rgba(58, 189, 159, 0.15);
    }
`;

which gives me the error No overload matches this call. and says ListItem.d.ts(24, 38): 'button' is declared here.. No error if I use it like <StyledListItem button>, but without button it does give an error. Idk if this is expected behaviour or not.

Having the same issue, I'm trying to do

    const unlocked = status !== "locked";
    return(<ListItem button={unlocked} />)

and I get the error message Type 'boolean' is not assignable to type 'true'..

Seems pretty counter-intuitive to me

@ErwanDL For me a cast worked:

` return(<ListItem button={unlocked as true | undefined} />)

Can't the evolution provided by TS 3.5 https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#smarter-union-type-checking provide a way to correct this?

Can't the evolution provided by TS 3.5 typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#smarter-union-type-checking provide a way to correct this?

Since we rely on function overloads the feature in 3.5 does not help us. It only seems to apply to object property checking.

The following "solution" doesn't fix the root problem with the TS MUI types but provides a way to temporarily overcome the typescript errors.

Declare a custom interface:
interface CustomMenuItemProps extends MuiMenuItemProps { button?: any; }

Create a new component wrapping the original material-ui component and pass in the TS interface:
<MuiMenuItem {...CustomMenuItemProps} ref={ref} > {children} </MuiMenuItem>

Landed here after trying to calculate if a ListItem should be a button based on props. After seeing the same thing @franklixuefei talked about, I wasn't sure how we're supposed to toggle this property.

I'd like to be able to do something like <ListItem button={!isDisabled}>.... Is the only workaround at this point to wrap the component as @ClovisLeb mentioned?

[Edit] Of course as soon as I typed this up I thought, "why not just make your bool 'any'?" That works. So now I'm using this to toggle the button:
const isButton: any = isDisabled || isLocked ? false : true;
<ListItem button={isButton} ...

As mentioned above it's just a downside of using boolean literals as a discriminator. It's necessary to conditionally show what props are available.

Either be explicit:

if (showButton) {
  return <ListItem button={true} />;
} else {
  return <ListItem button={false} />;
}

Or cast to any:

<ListItem button={showButton as any} />

Looks like a limitation of TypeScript. For more details see #15049. Will keep an eye on this one but you might have to use an any cast

@eps1lon thank you for pointing me to the same issue.

IMHO, casting with any might be not a nice solution due the following reason:

  • In typescript with eslint, If eslint allowing any in the following rule '@typescript-eslint/no-explicit-any': 'off', it can make someone produce/forgetting any types anywhere in the code. It is very nice to prevent using any because it can open a question of what is type shape this variable could be (Especially if the name of the variable is not meaningful enough).

Typescript sometimes makes me scratch my head as this issue does to me. I was thinking about using enum but not sure if it nice to make user import the enum in his code in order to use it. But on the other side, we don't need if else also (I might be wrong tho).

enum ClassOfBook {
  YES = 'yes',
  NO = 'no'
}

interface Book {
  isBook: ClassOfBook
}

interface OverloadedComponent {
  (props: Book): void;
}

declare const SomeComponent: OverloadedComponent;

function Library(props: Book) {
  const { isBook } = props;
  SomeComponent(props);

  SomeComponent({ isBook: ClassOfBook.YES });
}

Furthermore, ts can't compile styled ListItem with button prop that is set to inline false
https://codesandbox.io/s/damp-grass-3w9vs?file=/src/App.tsx

Was this page helpful?
0 / 5 - 0 ratings

Related issues

activatedgeek picture activatedgeek  ·  3Comments

finaiized picture finaiized  ·  3Comments

rbozan picture rbozan  ·  3Comments

pola88 picture pola88  ·  3Comments

mattmiddlesworth picture mattmiddlesworth  ·  3Comments