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] Constrain props type param appropriately in withStyles, withTheme, withWidth HOCs #11003

Merged
merged 17 commits into from
Apr 17, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 22 additions & 4 deletions src/styles/withStyles.d.ts
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 } from '..';
import { Theme } from './createMuiTheme';

/**
Expand Down Expand Up @@ -28,19 +30,35 @@ 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> {
classes?: Partial<ClassNameMap<ClassKey>>;
innerRef?: React.Ref<any>;
}

/*
The Omit<P, 'classes'> below (rather than the canonical Omit<P, WithStyles<ClassKey>>)
is used to allow interoperability with withTheme, which may provide the 'theme' prop.
*/
/*
export default function withStyles<ClassKey extends string>(
style: StyleRules<ClassKey> | StyleRulesCallback<ClassKey>,
options?: WithStylesOptions,
): <P extends WithStyles<ClassKey>>(
component: React.ComponentType<P>,
) => React.ComponentType<Omit<P, 'classes'> & StyledComponentProps<ClassKey>>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's some merit here, but P should not be required to included a classes prop. Instead it should be required that if P does include a classes prop, it should have the right type. Something like this:

interface MaybeWithStyles extends Partial<WithStyles<ClassKey>> {
  [k: string]: any;
}

export default function withStyles<ClassKey extends string>(
  style: StyleRules<ClassKey> | StyleRulesCallback<ClassKey>,
  options?: WithStylesOptions,
): <P extends MaybeWithStyles<ClassKey>>(
  component: React.ComponentType<P & WithStyles<ClassKey>>,
) => React.ComponentType<Omit<P, 'classes'> & StyledComponentProps<ClassKey>>;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks great! Let me check it out a bit.

*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's get rid of this instead of commenting out.


interface MaybeWithStyles<ClassKey extends string> extends Partial<WithStyles<ClassKey>> {
[k: string]: any;
}
Copy link
Member

@pelotom pelotom Apr 14, 2018

Choose a reason for hiding this comment

The 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 withStyles, withWidth and withTheme:

P extends ConsistentWith<WithStyles<ClassKey>>
P extends ConsistentWith<WithWidthProps>
P extends ConsistentWith<WithTheme>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon


export default function withStyles<ClassKey extends string>(
style: StyleRules<ClassKey> | StyleRulesCallback<ClassKey>,
options?: WithStylesOptions,
): <P>(
): <P extends MaybeWithStyles<ClassKey>>(
component: React.ComponentType<P & WithStyles<ClassKey>>,
) => React.ComponentType<P & StyledComponentProps<ClassKey>>;
) => React.ComponentType<Omit<P, 'classes'> & StyledComponentProps<ClassKey>>;
7 changes: 4 additions & 3 deletions src/styles/withTheme.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Theme } from './createMuiTheme';
import { Omit } from '..';

export interface WithTheme {
theme: Theme;
}

declare const withTheme: () => <P>(
component: React.ComponentType<P & WithTheme>,
) => React.ComponentClass<P>;
declare const withTheme: () => <P extends WithTheme>(
component: React.ComponentType<P>,
) => React.ComponentClass<Omit<P, keyof WithTheme>>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, this shouldn't require P to contain theme...


export default withTheme;
7 changes: 4 additions & 3 deletions src/utils/withWidth.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Breakpoint } from '../styles/createBreakpoints';
import { Omit } from '..';

export interface WithWidthOptions {
resizeInterval: number;
Expand All @@ -22,6 +23,6 @@ export function isWidthUp(

export default function withWidth(
options?: WithWidthOptions,
): <P>(
component: React.ComponentType<P & WithWidthProps>,
) => React.ComponentClass<P & Partial<WithWidthProps>>;
): <P extends WithWidthProps>(
component: React.ComponentType<P>,
) => React.ComponentClass<Omit<P, keyof WithWidthProps> & Partial<WithWidthProps>>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, this shouldn't require P to contain width:

export default function withWidth(
  options?: WithWidthOptions,
): <P extends Partial<WithWidthProps> & Record<string, any>>(
  component: React.ComponentType<P & WithWidthProps>,
) => React.ComponentClass<Omit<P, keyof WithWidthProps> & Partial<WithWidthProps>>;

With all of these HOCs we want to enable this kind of pattern:

const Component = withWidth()<{
  // shouldn't need to specify width here; it's a given
  name: string
}>(({ width, name }) =>
  <div style={{ width }}>hello, {name}</div>
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wondered about those two, should have hit them. I'll add a couple of tests, too.

2 changes: 1 addition & 1 deletion test/typescript/components.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@ const TableTest = () => {
);
}

return withStyles(styles)<{}>(BasicTable);
return withStyles(styles)(BasicTable);
};

const TabsTest = () => {
Expand Down
2 changes: 1 addition & 1 deletion test/typescript/styles.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const ComponentWithChildren: React.SFC<WithStyles<ComponentClassNames>> = ({
children,
}) => <div className={classes.root}>{children}</div>;

const StyledExampleThree = withStyles(styleRule)<{}>(ComponentWithChildren);
const StyledExampleThree = withStyles(styleRule)(ComponentWithChildren);
<StyledExampleThree />;

// Also works with a plain object
Expand Down
2 changes: 1 addition & 1 deletion test/typescript/styling-comparison.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,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>;
Expand Down