Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[typescript] Smarter logic? #8063

Closed
nenadalm opened this issue Sep 6, 2017 · 30 comments
Closed

[typescript] Smarter logic? #8063

nenadalm opened this issue Sep 6, 2017 · 30 comments

Comments

@nenadalm
Copy link

nenadalm commented Sep 6, 2017

Problem description

Button has href prop. If that prop is set, a element is used to render the button (https://material-ui-1dab0.firebaseapp.com/api/button/). It would be nice if Button had also target property.

Versions

  • Material-UI: 1.0.0-beta.8
@oliviertassinari
Copy link
Member

Any other properties supplied will be spread to the root element.

So you can provide a target property too.

@oliviertassinari oliviertassinari added the support: question Community support but can be turned into an improvement label Sep 6, 2017
@nenadalm
Copy link
Author

nenadalm commented Sep 6, 2017

@oliviertassinari I can't because I am using typescript and you have incorrect typings, so it complains that property target does not exists.

@oliviertassinari oliviertassinari changed the title Cannot specify target of button. [typescript] Smarter logic? Sep 6, 2017
@oliviertassinari oliviertassinari added typescript v1 and removed support: question Community support but can be turned into an improvement labels Sep 6, 2017
@oliviertassinari
Copy link
Member

This is also linked to #8043

@oliviertassinari
Copy link
Member

cc @benbayard

@mctep
Copy link
Contributor

mctep commented Sep 6, 2017

@benbayard I think this way should work for router links:

<Button component={props => <RouterLink {...props} to="/" />} />

@nenadalm and also for simple link:

<Button component={props => <a {...props} href="/" target="_blank" />} />

@oliviertassinari In TS typings component prop for Button should be React.ReactType not React.ReactNode

@sebald
Copy link
Member

sebald commented Sep 6, 2017

We had this "smarter logic" in the typings, but it didn't work out like we wanted it to :-/
You can read the reason why here: microsoft/TypeScript#17882

The only thing we can do is to allow all possiblities on the component. Meaning, allow all props from <a>.

@xaviergonz
Copy link
Contributor

xaviergonz commented Sep 8, 2017

Talking about typings, ButtonProps (and probable others) for example miss style?: React.CSSProperties from its typing, and also some (all?) components miss stuff available to all react components such as tabIndex.

v0.xx types were ok in this regard.

PS: the original typings extend their props with extends React.HTMLAttributes<{}> to get this behavior right

@benbayard
Copy link

benbayard commented Sep 8, 2017

@mctep That looks fine to be, but it should be noted that inline lambdas are highly unperformant. If I were to write docs for this I would write it as:

function buttonLinkCreator(to: string) {
  return (props: ButtonProps) => <Link {...props} to={to} />
}

const HomePageLink = buttonLinkCreator('/');

class Blah extends React.Component {
  render() {
    return <Button component={HomePageLink} />;
  }
}

@sebald
Copy link
Member

sebald commented Sep 11, 2017

@xaviergonz Thanks for noting that. The new typings actually function the same way. If we missed one, please open up an issue! :) You already did, thanks!

@ALL I guess the easy fix would just be to make ButtonBase inherit from ButtonHTMLAttributes & AnchorHTMLAttributes. I am still hesitant to allow any props, which would mean adding the index signature to the ButtonBaseProps. If we do this, there is no protection against typos anymore.

What you all think?

@nenadalm
Copy link
Author

Best would be to type it as good as typescript allows (so whitelist all possible allowed properties).

@xaviergonz
Copy link
Contributor

xaviergonz commented Sep 11, 2017

What about using Pick from the react types we may need?
Pick is used like this:

interface Task {
 id: string,
 name: string,
 assignee: string,
 contacts: any[], //for brevity
 associatedJob: string,
 submissionDate: string,
 allocatedTime: number,
 expectedCompletion: string,
 invoiceNumber: string,
 invoiceDueDate: string,
 comment: string,
 taskAddress: string
 ...
}

type PartialTast = Pick<Task, 'id' | 'name' | 'contacts'> // an interface with only those properties from the original interface

so we could do something like

interface ButtonProps extends Pick<
  React.ButtonHTMLAttributes<HTMLButtonElement>, 
  'id' | 'className' | 'style' | 'title' | 'tabIndex' | 'disabled'
> {
  x: number;
  // whatever custom props including children
}

that way the types would be always in sync with react ones.

@benbayard
Copy link

@xaviergonz Why are we picking some properties out of button? Is it because there are some that are not supported. If so, you can use an Omit type which looks like this. It's much easier to maintain this way.

  /**
   * From https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-307871458
   * The Diff type is a subtraction operator for string literal types. It relies on:
   *  - T | never = T
   *  - T & never = never
   *  - An object with a string index signature can be indexed with any string.
   */
  export type StringDiff<T extends string, U extends string> = (
    {[K in T]: K} &
    {[K in U]: never} & {[K: string]: never}
  )[T];

  /**
   * From https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-311923766
   * Omits keys in K from object type T
   */
  export type ObjectOmit<T extends object, K extends keyof T> = Pick<T, StringDiff<keyof T, K>>;

@xaviergonz
Copy link
Contributor

I didn't even know that was possible in TS oO

@sebald
Copy link
Member

sebald commented Sep 12, 2017

We're actually using this because some of the native APIs and the APIs of material-ui are sometimes not compatible 😉

https://github.com/callemall/material-ui/blob/1d84cb89a97755245d6b02921e983b39e4468204/src/Radio/RadioGroup.d.ts#L1-L11


But I think this discussion goes a little bit in the wrong direction. Why should we pick or omit things from the native elements.

@nenadalm wrote:

Best would be to type it as good as typescript allows (so whitelist all possible allowed properties).

This is actually why we didn't decide on that yet and the typings are (actually) kinda broken. IMHO the typings for ButtonBase should have the API provided by material-ui + <button> + <a>. If someone is (ab-)using the component prop and needs more/different API s/he can still to this, but loses type-safety (has to use as any). My guess is that the later will only be rarely used. Most people will use the components as a button or an anchor and we should primarily support this case and try to have as type-safe typings as possible, which we would have in my opinion we the aforementioned approach.

@xaviergonz
Copy link
Contributor

I'd rather have the code compile and maybe get some runtime warnings from proptypes than having to use hacks like what I'm using right now for untyped properties, e.g:

<Button 
  color="primary"
  {...{
    title: "A button tooltip" // this works but is never type checked :(
  }}
>
  hi
</Button>

@benbayard
Copy link

@sebald I asked the same question, why are we picking any thing? (I'd prefer omiting over picking).

I think the component prop is perfect for creating a type-safe component until typescript can get bi-furcating types.

@sebald
Copy link
Member

sebald commented Sep 13, 2017

@xaviergonz With the proposed approach this would actually not be necessary, since title and a ton of other basic HTML attributes are covered by React.HTMLAttributes.

@benbayard Could you explain to me what you mean by "the component prop is perfect for creating a type-safe component"?

@sebald
Copy link
Member

sebald commented Sep 13, 2017

I made a PR with the mentioned approach, so when @oliviertassinari cuts a release on the weekend the typings will (hopefully) be better for everyone. If this is not enough, I'll add the index signature.

Notet that this will affect the following components:

  • ButtonNavigationButton
  • Butotn
  • ButtonBase
  • IconButotn
  • ListItem
  • TableSortLabel
  • Tab
  • Tabs
  • TabScrollButton

@oliviertassinari
Copy link
Member

@sebald Great! :)

@stunaz
Copy link
Contributor

stunaz commented Nov 7, 2017

Hi, How would you type/fix this :

<ListItem
    component={Link}
    to="/about"
>

I have the following error:
file: 'file:///Users/stunaz/Sites/pegase-react-ts/src/modules/demo/Demo.tsx' severity: 'Error' message: 'Property 'to' does not exist on type 'IntrinsicAttributes & ListItemProps & { children?: ReactNode; }'.' at: '78,37' source: 'ts'

@stunaz
Copy link
Contributor

stunaz commented Nov 7, 2017

@pelotom @sebald is the above supported ? if no is there a way around it?

@sebald
Copy link
Member

sebald commented Nov 7, 2017

You can always use spread and any as a workaround. This should work

<ListItem
    component={Link}
    {...{ to: '/about' } as any}
>

Though you will lose type safety.


I am not sure if to was ever support 😐 But I guess we could try to use tagged union types, to have better typings.

@stunaz
Copy link
Contributor

stunaz commented Nov 7, 2017

to is a props used for the Link Component (react-router).
Currently with the ListItem I could pass as component={MyOwnComponent} and add additionals props destined for MyOwnComponent.

Ideally ListItem should accept his own props and props from the component'props.

The same goes for several others component as Grid, Card, Paper ...

@pelotom
Copy link
Member

pelotom commented Nov 7, 2017

It might be worth considering adding

  button?: boolean;
  component?: React.ReactType;
+ additionalProps: {};
  dense?: boolean;
  disabled?: boolean;
  disableGutters?: boolean;
  divider?: boolean;

to ListItemProps and likewise all other components that allow overriding their root component. Then you could do

<ListItem
  component={Link}
  additionalProps={{ to: "/about" }}
/>

without being hampered by the type system. An escape hatch like this would allow us to avoid compromising the strict typing of the rest of the props, and just say, "if you're providing your own component you can throw whatever extra properties you like in here... use at your own risk!"

This will of course require a corresponding change to the .js implementations of all these components.

@pelotom
Copy link
Member

pelotom commented Nov 7, 2017

Here's a less invasive and more type safe idea: define a HOC

declare const overrideComponent:
  <BaseProps extends { component?: React.ReactType}>(BaseComponent: React.ComponentType<BaseProps>) =>
  <CustomProps>(CustomComponent: React.ComponentType<CustomProps>) =>
  React.ComponentType<Replace<BaseProps, CustomProps>>

whose implementation is just

const overrideComponent = BaseComponent => CustomComponent => props =>
  <BaseComponent component={CustomComponent} {...props} />

and then you can do

const LinkListItem = overrideComponent(ListItem)(Link)

const elem = <LinkListItem to="/about" />

and it's completely type safe. On the decorated component you can provide all properties from the base component as well as the custom component, which matches the underlying semantics.

@oliviertassinari
Copy link
Member

oliviertassinari commented Nov 7, 2017

A better alternative where you will not risk a property collision:

<ListItem component={props => <Link {...props} to="/about" />}>
  // ...
</ListItem>

I have always considered the below pattern an elegant "hack" (it's harder to understand).
But what if both ListItem and Link implement a disabled property that you don't want to share the same value between? You use the pattern above, not below.

<ListItem component={Link} to="/about">
  // ...
</ListItem>

It's also why you won't find this third API with Material-UI. It's a tradeoff between the two previous ones that suffer limitations without being much simpler.

<ListItem component={<Link to="/about" />}>
  // ...
</ListItem>

@pelotom
Copy link
Member

pelotom commented Nov 7, 2017

@oliviertassinari that's a great way to do it!

@stunaz
Copy link
Contributor

stunaz commented Nov 7, 2017

just tested @oliviertassinari solution, worked fine, no more typings issue 👍
thanks all again

@activebiz
Copy link

This no longer works as per "@types/react": "^16.0.36",

@sebald
Copy link
Member

sebald commented Feb 12, 2018

If you're running into issues using Material-UI, please don't reply to old and closed issues. Open up a new one and provide as much info as possible. This way it's more likely we can help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants