-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
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] Fix with* injectors ignoring defaultProps #12673
Changes from 9 commits
5c36cb0
d6d88bb
25b333b
a10f121
917f221
93da0df
5da60a0
8c83e14
eed5c48
859d2d9
a0ffe02
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 |
---|---|---|
|
@@ -64,6 +64,18 @@ export type ConsistentWith<T, U> = Pick<U, keyof T & keyof U>; | |
*/ | ||
export type Overwrite<T, U> = Omit<T, keyof U> & U; | ||
|
||
/** | ||
* a property P will will have the the type of: | ||
* - DecorationTargetProps if it is not present in InjectedProps | ||
* - InjectedTargetProps if it is present in DecorationTargetProps | ||
* | ||
*/ | ||
export type Matching<InjectedProps, DecorationTargetProps> = { | ||
[P in keyof DecorationTargetProps]: P extends keyof InjectedProps | ||
? InjectedProps[P] | ||
: DecorationTargetProps[P] | ||
}; | ||
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. It seems like 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 thought so too but |
||
|
||
export namespace PropTypes { | ||
type Alignment = 'inherit' | 'left' | 'center' | 'right' | 'justify'; | ||
type Color = 'inherit' | 'primary' | 'secondary' | 'default'; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import * as React from 'react'; | ||
import { WithTheme } from '../styles/withTheme'; | ||
import { AnyComponent, ConsistentWith, Overwrite, Omit } from '..'; | ||
import { Matching, Omit, PropsOf } from '..'; | ||
import { Theme } from './createMuiTheme'; | ||
import * as CSS from 'csstype'; | ||
import * as JSS from 'jss'; | ||
|
@@ -60,6 +60,14 @@ export default function withStyles< | |
>( | ||
style: StyleRulesCallback<ClassKey> | StyleRules<ClassKey>, | ||
options?: Options, | ||
): <P extends ConsistentWith<P, StyledComponentProps<ClassKey> & Partial<WithTheme>>>( | ||
component: AnyComponent<P & WithStyles<ClassKey, Options['withTheme']>>, | ||
) => React.ComponentType<Overwrite<Omit<P, 'theme'>, StyledComponentProps<ClassKey>>>; | ||
): < | ||
C extends React.ComponentType<Matching<WithStyles<ClassKey, Options['withTheme']>, PropsOf<C>>> | ||
>( | ||
component: C, | ||
) => React.ComponentType< | ||
Omit< | ||
JSX.LibraryManagedAttributes<C, PropsOf<C>>, | ||
keyof WithStyles<ClassKey, Options['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. We only ever use Omit<
JSX.LibraryManagedAttributes<C, PropsOf<C>>,
(keyof WithStyles<ClassKey, Options['withTheme']>) & (keyof PropsOf<C>)
> here. Furthermore the Omit<
JSX.LibraryManagedAttributes<C, PropsOf<C>>,
keyof WithStyles<ClassKey, Options['withTheme']>
> |
||
StyledComponentProps<ClassKey> | ||
>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,11 +14,11 @@ import { | |
import Button from '@material-ui/core/Button/Button'; | ||
import blue from '@material-ui/core/colors/blue'; | ||
import { WithTheme } from '@material-ui/core/styles/withTheme'; | ||
import { StandardProps } from '@material-ui/core'; | ||
import { Matching, PropsOf, StandardProps } from '@material-ui/core'; | ||
import { TypographyStyle } from '@material-ui/core/styles/createTypography'; | ||
|
||
// Shared types for examples | ||
interface ComponentProps { | ||
interface ComponentProps extends WithStyles<typeof styles> { | ||
text: string; | ||
} | ||
|
||
|
@@ -31,7 +31,7 @@ const styles = ({ palette, spacing }: Theme) => ({ | |
}, | ||
}); | ||
|
||
const StyledExampleOne = withStyles(styles)<ComponentProps>(({ classes, text }) => ( | ||
const StyledExampleOne = withStyles(styles)(({ classes, text }: ComponentProps) => ( | ||
<div className={classes.root}>{text}</div> | ||
)); | ||
<StyledExampleOne text="I am styled!" />; | ||
|
@@ -70,7 +70,7 @@ const stylesAsPojo = { | |
|
||
const AnotherStyledSFC = withStyles({ | ||
root: { backgroundColor: 'hotpink' }, | ||
})(({ classes }) => <div className={classes.root}>Stylish!</div>); | ||
})(({ classes }: WithStyles<'root'>) => <div className={classes.root}>Stylish!</div>); | ||
|
||
// Overriding styles | ||
const theme = createMuiTheme({ | ||
|
@@ -170,7 +170,7 @@ const ComponentWithTheme = withTheme()(({ theme }) => <div>{theme.spacing.unit}< | |
type AllTheProps = WithTheme & WithStyles<typeof styles>; | ||
|
||
const AllTheComposition = withTheme()( | ||
withStyles(styles)(({ theme, classes }: AllTheProps) => ( | ||
withStyles(styles, { withTheme: true })(({ theme, classes }: AllTheProps) => ( | ||
<div className={classes.root}>{theme.palette.text.primary}</div> | ||
)), | ||
); | ||
|
@@ -201,7 +201,7 @@ declare const themed: boolean; | |
); | ||
<Foo />; | ||
|
||
const Bar = withStyles({}, { withTheme: true })(({ theme }) => ( | ||
const Bar = withStyles({}, { withTheme: true })(({ theme }: WithStyles<string, true>) => ( | ||
<div style={{ margin: theme.spacing.unit }} /> | ||
)); | ||
<Bar />; | ||
|
@@ -292,12 +292,13 @@ withStyles(theme => | |
}); | ||
|
||
interface ListItemContentProps extends WithStyles<typeof styles> { | ||
children?: React.ReactElement<any>; | ||
inset?: boolean; | ||
row?: boolean; | ||
} | ||
|
||
const ListItemContent = withStyles(styles, { name: 'ui-ListItemContent' })<ListItemContentProps>( | ||
({ children, classes, inset, row }) => ( | ||
const ListItemContent = withStyles(styles, { name: 'ui-ListItemContent' })( | ||
({ children, classes, inset, row }: ListItemContentProps) => ( | ||
<div className={classes.root} color="textSecondary"> | ||
{children} | ||
</div> | ||
|
@@ -311,7 +312,7 @@ withStyles(theme => | |
b: boolean; | ||
} | ||
|
||
const ListItemContent = withStyles({ x: {}, y: {} })<FooProps>(props => <div />); | ||
const ListItemContent = withStyles({ x: {}, y: {} })((props: FooProps) => <div />); | ||
} | ||
|
||
{ | ||
|
@@ -383,3 +384,65 @@ withStyles(theme => | |
text: theme.typography.body2, | ||
}); | ||
} | ||
|
||
{ | ||
// can't provide own `classes` type | ||
interface Props { | ||
classes: number; | ||
} | ||
|
||
class Component extends React.Component<Props & WithStyles<typeof styles>> {} | ||
// $ExpectError | ||
const StyledComponent = withStyles(styles)(Component); | ||
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. This consistency check isn't working when applied to an SFC: // $ExpectError
withStyles(styles)((props: Props) => null); 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. @pelotom This is a limitation of the If I type this function explicitly as a I'm going to verify if this was working before. If we can't resolve this we have to make a decision whether we want to support 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. It did indeed work. I would prefer 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. Hm, are you 100% sure that we can't support both? I haven't looked at this in enough detail to know... but I agree that 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. Not with the I probably revisit the react-redux typings since I used the But as is the issue with #12697 this problem at least has a workaround with explicit typing. 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. If we can't do a complete job of enforcing consistency maybe we should just remove the constraints completely, as they significantly complicate the expression of the types, and arguably they're of fairly niche value. 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. @eps1lon as the original author on the react-redux PR, happy to help out if I can. I haven't gotten any reviews in 20 days so it's honestly been a bit hard to iterate/improve on the definition. If there are edge cases it doesn't handle, I can certainly iterate. I also have my email in my github profile so feel free to reach out that way. 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. FWIW, is the issue you're dealing with related to this by any chance DefinitelyTyped/DefinitelyTyped#28249? 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 issue is probably causing the fail with the explicitly typed SFC. So if that issue is resolved we might not even have a consistency check for explicitly typed SFCs. |
||
|
||
// implicit SFC | ||
withStyles(styles)((props: Props) => null); // $ExpectError | ||
withStyles(styles)((props: Props & WithStyles<typeof styles>) => null); // $ExpectError | ||
withStyles(styles)((props: Props & { children?: React.ReactNode }) => null); // $ExpectError | ||
withStyles(styles)( | ||
(props: Props & WithStyles<typeof styles> & { children?: React.ReactNode }) => null, // $ExpectError | ||
); | ||
|
||
// explicit not but with "Property 'children' is missing in type 'ValidationMap<Props>'". | ||
// which is not helpful | ||
const StatelessComponent: React.SFC<Props> = props => null; | ||
const StatelessComponentWithStyles: React.SFC<Props & WithStyles<typeof styles>> = props => null; | ||
withStyles(styles)(StatelessComponent); // $ExpectError | ||
withStyles(styles)(StatelessComponentWithStyles); // $ExpectError | ||
} | ||
|
||
{ | ||
// https://github.com/mui-org/material-ui/issues/12670 | ||
interface Props { | ||
nonDefaulted: string; | ||
defaulted: number; | ||
} | ||
|
||
class MyButton extends React.Component<Props & WithStyles<typeof styles>> { | ||
static defaultProps = { | ||
defaulted: 0, | ||
}; | ||
|
||
render() { | ||
const { classes, nonDefaulted, defaulted } = this.props; | ||
return ( | ||
<Button className={classes.btn}> | ||
{defaulted}, {nonDefaulted} | ||
</Button> | ||
); | ||
} | ||
} | ||
|
||
const styles = () => | ||
createStyles({ | ||
btn: { | ||
color: 'red', | ||
}, | ||
}); | ||
|
||
const StyledMyButton = withStyles(styles)(MyButton); | ||
|
||
const CorrectUsage = () => <StyledMyButton nonDefaulted="2" />; | ||
// Property 'nonDefaulted' is missing in type '{}' | ||
const MissingPropUsage = () => <StyledMyButton />; // $ExpectError | ||
} |
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.
This no longer validates that the two interfaces passed in are matching. If
InjectedProps[P]
doesn't extendDecorationTargetProps[P]
(assuming their keys are shared) then that means that the two definitions are incompatible and cannot be reconciled. E.g., this would accept:Matching<{ test: string; }, { test: boolean; }>
Edit: actually, you can ignore me, I think I see what you're doing? Maybe... Need to test it locally.
Edit2: Nope, this is definitely incorrect but not for the reason I mentioned. The problem is that if your component declares something as optional and the InjectedProps spec something as required then it won't match.
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.
@apapirovski
Which is sufficient for usage in
withStyles
. The previous version allowed mismatch of props for functional components that did not have theReact.SFC
type or to be more specific only had a call signature. I'm going to checkout the react/redux typings with your PR and see if this happens there to.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.
I posted a new definition for Matching in my PR that is IMO slightly more correct, at least as far as
connect
goes. Not sure if you have different requirements here as I don't use material-ui. Thanks for the test case and the triage!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.
@apapirovski
I agree that approach is indeed better and I tried this before but this caused some regression. I realized that this was not an issue with your definition but with ours. Basically injecting
T | undefined
while we actually only injectT
if it is defined which is different when using spread syntax.