-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
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] Constrain props type param appropriately in withStyles, withTheme, withWidth HOCs #11003
[typescript] Constrain props type param appropriately in withStyles, withTheme, withWidth HOCs #11003
Changes from 9 commits
e1f809d
f488adf
3560ac0
8eb517a
175fa49
92bee1b
c01d787
89d2a60
bcbf701
9fbbbc1
e66b8ea
0b47fb3
748af39
b24250b
ac934c1
f1dbf16
ea20df9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ Have a look at the [Create React App with TypeScript](https://github.com/mui-org | |
The usage of `withStyles` in TypeScript can be a little tricky, so it's worth showing some examples. You can first call `withStyles()` to create a decorator function, like so: | ||
|
||
```js | ||
const decorate = withStyles(({ palette, spacing }) => ({ | ||
const withMyStyles = withStyles(({ palette, spacing }) => ({ | ||
root: { | ||
padding: spacing.unit, | ||
backgroundColor: palette.background.default, | ||
|
@@ -30,7 +30,7 @@ interface Props { | |
Functional components are straightforward: | ||
|
||
```jsx | ||
const DecoratedSFC = decorate<Props>(({ text, type, color, classes }) => ( | ||
const DecoratedSFC = withMyStyles<Props>(({ text, type, color, classes }) => ( | ||
<Typography variant={type} color={color} classes={classes}> | ||
{text} | ||
</Typography> | ||
|
@@ -42,7 +42,7 @@ Class components are a little more cumbersome. Due to a [current limitation in T | |
```jsx | ||
import { WithStyles } from 'material-ui/styles'; | ||
|
||
const DecoratedClass = decorate( | ||
const DecoratedClass = withMyStyles( | ||
class extends React.Component<Props & WithStyles<'root'>> { | ||
render() { | ||
const { text, type, color, classes } = this.props | ||
|
@@ -56,45 +56,8 @@ const DecoratedClass = decorate( | |
); | ||
``` | ||
|
||
Note that in the class example you didn't need to annotate `<Props>` in the call to `decorate`; type inference took care of everything. However, there are 2 scenarios where you _do_ need to provide an explicit type argument to `decorate`. | ||
|
||
Scenario 1: your styled component takes _no_ additional props in addition to `classes`. The natural thing would be to write: | ||
|
||
```jsx | ||
import { WithStyles } from 'material-ui/styles'; | ||
|
||
const DecoratedNoProps = decorate( | ||
class extends React.Component<WithStyles<'root'>> { | ||
render() { | ||
return ( | ||
<Typography classes={this.props.classes}> | ||
Hello, World! | ||
</Typography> | ||
); | ||
} | ||
} | ||
); | ||
``` | ||
|
||
Unfortunately, TypeScript infers the wrong type in this case and you'll have trouble when you go to make an element of this component. In this case, you'll need to provide an explicit `{}` type argument, like so: | ||
|
||
```jsx | ||
import { WithStyles } from 'material-ui/styles'; | ||
|
||
const DecoratedNoProps = decorate<{}>( // <-- note the type argument! | ||
class extends React.Component<WithStyles<'root'>> { | ||
render() { | ||
return ( | ||
<Typography classes={this.props.classes}> | ||
Hello, World! | ||
</Typography> | ||
); | ||
} | ||
} | ||
); | ||
``` | ||
|
||
Scenario 2: `Props` is a union type. Again, to avoid getting a compiler error, you'll need to provide an explict type argument: | ||
When your `props` are a union, Typescript needs you to explicitly tell it the type, | ||
by providing an explicit generic `<Props>` parameter to `withMyStyles`: | ||
|
||
```jsx | ||
import { WithStyles } from 'material-ui/styles'; | ||
|
@@ -111,7 +74,7 @@ interface Painting { | |
|
||
type Props = Book | Painting; | ||
|
||
const DecoratedUnionProps = decorate<Props>( // <-- without the type argument, we'd get a compiler error! | ||
const DecoratedUnionProps = withMyStyles<Props>( // <-- without the type argument, we'd get a compiler error! | ||
class extends React.Component<Props & WithStyles<'root'>> { | ||
render() { | ||
const props = this.props; | ||
|
@@ -125,7 +88,6 @@ const DecoratedUnionProps = decorate<Props>( // <-- without the type argument, w | |
); | ||
``` | ||
|
||
To avoid worrying about these 2 edge cases, it may be a good habit to always provide an explicit type argument to `decorate`. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discussion of the second (now only) scenario needs to be restored here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think not, if the compiler version can be moved forward. See below. |
||
### Injecting Multiple Classes | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,7 +46,7 @@ | |
"test:coverage:html": "cross-env NODE_ENV=test BABEL_ENV=coverage nyc mocha test/**/*.test.js src/{,**/}*.test.js && nyc report --reporter=html", | ||
"test:karma": "cross-env NODE_ENV=test karma start test/karma.conf.js --single-run", | ||
"test:regressions": "webpack --config test/regressions/webpack.config.js && rimraf test/regressions/screenshots/chrome/* && vrtest run --config test/vrtest.config.js --record", | ||
"typescript": "tsc -p tsconfig.json", | ||
"typescript": "tsc -v && tsc -p tsconfig.json", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert. |
||
"flow": "flow --show-all-errors", | ||
"argos": "argos upload test/regressions/screenshots/chrome --token $ARGOS_TOKEN || true", | ||
"prebuild": "rimraf build", | ||
|
@@ -187,7 +187,7 @@ | |
"rimraf": "^2.6.2", | ||
"sinon": "^4.1.2", | ||
"size-limit": "0.14.1", | ||
"typescript": "^2.6.1", | ||
"typescript": "2.6.2", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, but again, it doesn't compile in 2.6.1. Do you want me to remove the fragment from the test? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That doesn't matter, the |
||
"url-loader": "^1.0.1", | ||
"vrtest": "^0.2.0", | ||
"webfontloader": "^1.6.28", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
import * as React from 'react'; | ||
import { WithTheme } from '../styles/withTheme'; | ||
import { Omit, ConsistentWith } from '..'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
import { Theme } from './createMuiTheme'; | ||
|
||
/** | ||
|
@@ -28,9 +30,8 @@ export interface WithStylesOptions { | |
|
||
export type ClassNameMap<ClassKey extends string = string> = Record<ClassKey, string>; | ||
|
||
export interface WithStyles<ClassKey extends string = string> { | ||
export interface WithStyles<ClassKey extends string = string> extends Partial<WithTheme> { | ||
classes: ClassNameMap<ClassKey>; | ||
theme?: Theme; | ||
} | ||
|
||
export interface StyledComponentProps<ClassKey extends string = string> { | ||
|
@@ -41,6 +42,11 @@ export interface StyledComponentProps<ClassKey extends string = string> { | |
export default function withStyles<ClassKey extends string>( | ||
style: StyleRules<ClassKey> | StyleRulesCallback<ClassKey>, | ||
options?: WithStylesOptions, | ||
): <P>( | ||
component: React.ComponentType<P & WithStyles<ClassKey>>, | ||
) => React.ComponentType<P & StyledComponentProps<ClassKey>>; | ||
): { | ||
( | ||
component: React.ComponentType<WithStyles<ClassKey>>, | ||
): React.ComponentType<StyledComponentProps<ClassKey>>; | ||
<P extends ConsistentWith<WithStyles<ClassKey>>>( | ||
component: React.ComponentType<P & WithStyles<ClassKey>>, | ||
): React.ComponentType<P & StyledComponentProps<ClassKey>>; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about making a reusable version of this, type ConsistentWith<O> = Partial<O> & Record<string, any>; which can then be used for P extends ConsistentWith<WithStyles<ClassKey>>
P extends ConsistentWith<WithWidthProps>
P extends ConsistentWith<WithTheme> There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing semicolon |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import * as React from 'react'; | ||
import { Omit } from '../../src'; | ||
import Typography, { TypographyProps } from '../../src/Typography/Typography'; | ||
import { withStyles, WithStyles } from '../../src/styles'; | ||
|
||
|
@@ -35,7 +36,7 @@ const DecoratedClass = decorate( | |
}, | ||
); | ||
|
||
const DecoratedNoProps = decorate<{}>( | ||
const DecoratedNoProps = decorate( | ||
class extends React.Component<WithStyles<'root'>> { | ||
render() { | ||
return <Typography classes={this.props.classes}>Hello, World!</Typography>; | ||
|
@@ -46,3 +47,33 @@ const DecoratedNoProps = decorate<{}>( | |
const sfcElem = <DecoratedSFC text="Hello, World!" variant="title" color="secondary" />; | ||
const classElem = <DecoratedClass text="Hello, World!" variant="title" color="secondary" />; | ||
const noPropsElem = <DecoratedNoProps />; | ||
|
||
// This is the "scenario 2" example straight from the doc, then invoked: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no scenario 2 now, maybe just provide a link or else delete the comment. |
||
|
||
interface Book { | ||
category: 'book'; | ||
author: string; | ||
} | ||
|
||
interface Painting { | ||
category: 'painting'; | ||
artist: string; | ||
} | ||
|
||
type ArtProps = Book | Painting; | ||
|
||
const DecoratedUnionProps = decorate<ArtProps>( // <-- without the type argument, we'd get a compiler error! | ||
class extends React.Component<ArtProps & WithStyles<'root'>> { | ||
render() { | ||
const props = this.props; | ||
return ( | ||
<Typography classes={props.classes}> | ||
{props.category === "book" ? props.author : props.artist} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, done, but what is the intended preference? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My immediate preference is to keep irrelevant changes out of this PR 🙂 |
||
</Typography> | ||
); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
); | ||
|
||
const unionPropElem = <DecoratedUnionProps category="book" author="Twain, Mark" />; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10173,9 +10173,9 @@ typedarray@^0.0.6, typedarray@~0.0.5: | |
version "0.0.6" | ||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" | ||
|
||
typescript@^2.6.1: | ||
version "2.8.1" | ||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.1.tgz#6160e4f8f195d5ba81d4876f9c0cc1fbc0820624" | ||
typescript@2.6.2: | ||
version "2.6.2" | ||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
|
||
ua-parser-js@^0.7.9: | ||
version "0.7.17" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't rename
decorate
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done