diff --git a/packages/material-ui/src/index.d.ts b/packages/material-ui/src/index.d.ts index 0f86086cdec6c2..39f57849c35a5e 100644 --- a/packages/material-ui/src/index.d.ts +++ b/packages/material-ui/src/index.d.ts @@ -64,6 +64,42 @@ export type ConsistentWith = Pick; */ export type Overwrite = Omit & U; +/** + * a property P will be present if: + * - it is present in DecorationTargetProps + * - it is present in InjectedProps[P] and can satisfy DecorationTargetProps[P] + * or it is not present in InjectedProps[P] + * + * source: @types/react-redux + */ +export type Matching = { + [P in keyof DecorationTargetProps]: P extends keyof InjectedProps + ? InjectedProps[P] extends DecorationTargetProps[P] ? DecorationTargetProps[P] : never + : DecorationTargetProps[P] +}; + +/** + * a property P will be present if : + * - it is present in both DecorationTargetProps and InjectedProps + * - InjectedProps[P] can satisfy DecorationTargetProps[P] + * ie: decorated component can accept more types than decorator is injecting + * + * For decoration, inject props or ownProps are all optionally + * required by the decorated (right hand side) component. + * But any property required by the decorated component must be satisfied by the injected property. + * + * source: @types/react-redux + */ +export type Shared< + InjectedProps, + DecorationTargetProps extends Shared +> = { + [P in Extract< + keyof InjectedProps, + keyof DecorationTargetProps + >]?: InjectedProps[P] extends DecorationTargetProps[P] ? DecorationTargetProps[P] : never +}; + export namespace PropTypes { type Alignment = 'inherit' | 'left' | 'center' | 'right' | 'justify'; type Color = 'inherit' | 'primary' | 'secondary' | 'default'; diff --git a/packages/material-ui/src/styles/withStyles.d.ts b/packages/material-ui/src/styles/withStyles.d.ts index fb2fa88ec9c8ea..21fc5d7edfb62f 100644 --- a/packages/material-ui/src/styles/withStyles.d.ts +++ b/packages/material-ui/src/styles/withStyles.d.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { WithTheme } from '../styles/withTheme'; -import { AnyComponent, ConsistentWith, Overwrite, Omit } from '..'; +import { Matching, Omit, PropsOf, Shared } 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 | StyleRules, options?: Options, -):

& Partial>>( - component: AnyComponent

>, -) => React.ComponentType, StyledComponentProps>>; +): < + C extends React.ComponentType, WithStyles>> +>( + component: C, +) => React.ComponentType< + Omit< + JSX.LibraryManagedAttributes>, + keyof Shared, PropsOf> + > & + StyledComponentProps +>; diff --git a/packages/material-ui/test/typescript/styles.spec.tsx b/packages/material-ui/test/typescript/styles.spec.tsx index 7260c618dbc753..c9348f4152afbf 100644 --- a/packages/material-ui/test/typescript/styles.spec.tsx +++ b/packages/material-ui/test/typescript/styles.spec.tsx @@ -18,7 +18,7 @@ import { StandardProps } from '@material-ui/core'; import { TypographyStyle } from '@material-ui/core/styles/createTypography'; // Shared types for examples -interface ComponentProps { +interface ComponentProps extends WithStyles { text: string; } @@ -31,7 +31,7 @@ const styles = ({ palette, spacing }: Theme) => ({ }, }); -const StyledExampleOne = withStyles(styles)(({ classes, text }) => ( +const StyledExampleOne = withStyles(styles)(({ classes, text }: ComponentProps) => (

{text}
)); ; @@ -70,7 +70,7 @@ const stylesAsPojo = { const AnotherStyledSFC = withStyles({ root: { backgroundColor: 'hotpink' }, -})(({ classes }) =>
Stylish!
); +})(({ classes }: WithStyles<'root'>) =>
Stylish!
); // Overriding styles const theme = createMuiTheme({ @@ -201,7 +201,7 @@ declare const themed: boolean; ); ; - const Bar = withStyles({}, { withTheme: true })(({ theme }) => ( + const Bar = withStyles({}, { withTheme: true })(({ theme }: WithStyles) => (
)); ; @@ -263,12 +263,13 @@ withStyles(theme => }); interface ListItemContentProps extends WithStyles { + children?: React.ReactElement; inset?: boolean; row?: boolean; } - const ListItemContent = withStyles(styles, { name: 'ui-ListItemContent' })( - ({ children, classes, inset, row }) => ( + const ListItemContent = withStyles(styles, { name: 'ui-ListItemContent' })( + ({ children, classes, inset, row }: ListItemContentProps) => (
{children}
@@ -282,7 +283,7 @@ withStyles(theme => b: boolean; } - const ListItemContent = withStyles({ x: {}, y: {} })(props =>
); + const ListItemContent = withStyles({ x: {}, y: {} })((props: FooProps) =>
); } { @@ -354,3 +355,76 @@ withStyles(theme => text: theme.typography.body2, }); } + +{ + // can't provide own `classes` type + interface Props { + classes: number; + } + + class Component extends React.Component> {} + // $ExpectError + const StyledComponent = withStyles(styles)(Component); +} + +{ + // https://github.com/mui-org/material-ui/issues/12670 + interface Props { + noDefault: string; + withDefaultProps: number; + } + + class MyButton extends React.Component> { + static defaultProps = { + withDefaultProps: 0, + }; + + render() { + const { classes, noDefault, withDefaultProps } = this.props; + return ( + + ); + } + } + + const styles = () => + createStyles({ + btn: { + color: 'red', + }, + }); + + const StyledMyButton = withStyles(styles)(MyButton); + + const CorrectUsage = () => ; + // Property 'noDefault' is missing in type '{}' + const MissingPropUsage = () => ; // $ExpectError +} + +{ + // union props + interface Book { + category: 'book'; + author: string; + } + interface Painting { + category: 'painting'; + artist: string; + } + type BookOrPainting = Book | Painting; + type Props = BookOrPainting & WithStyles; + const DecoratedUnionProps = withStyles(styles)( + class extends React.Component { + render() { + const props = this.props; + return ( +
+ {props.category === 'book' ? props.author : props.artist} +
+ ); + } + }, + ); +}