Skip to content

Commit

Permalink
[typescript] Style typing improvements (#12492)
Browse files Browse the repository at this point in the history
* Make Omit work with unions

* Use React.ComponentType instead of React.ComponentClass

* Rename WithWidthProps to WithWidth for consistency with WithTheme and WithStyles

* Add AnyComponent and WithProps helpers to eliminate need for type annotation

* Get the typing right when passing withTheme: true

* Remove caveat about union props from the docs

* Tweak variable names

* Formatting
  • Loading branch information
pelotom authored Aug 13, 2018
1 parent 26aaa8d commit 5a0bf7b
Show file tree
Hide file tree
Showing 10 changed files with 65 additions and 64 deletions.
33 changes: 0 additions & 33 deletions docs/src/pages/guides/typescript/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,39 +146,6 @@ const DecoratedClass = withStyles(styles)(

Unfortunately due to a [current limitation of TypeScript decorators](https://github.com/Microsoft/TypeScript/issues/4881), `withStyles(styles)` can't be used as a decorator in TypeScript.

### Union props

When your `props` are a union, Typescript needs you to explicitly tell it the type, by providing a generic `<Props>` parameter to `decorate`:

```tsx
interface Book {
category: "book";
author: string;
}

interface Painting {
category: "painting";
artist: string;
}

type BookOrPainting = Book | Painting;

type Props = BookOrPainting & WithStyles<typeof styles>;

const DecoratedUnionProps = withStyles(styles)<BookOrPainting>( // <-- without the type argument, we'd get a compiler error!
class extends React.Component<Props> {
render() {
const props = this.props;
return (
<Typography classes={props.classes}>
{props.category === "book" ? props.author : props.artist}
</Typography>
);
}
}
);
```

## Customization of `Theme`

When adding custom properties to the `Theme`, you may continue to use it in a strongly typed way by exploiting
Expand Down
3 changes: 1 addition & 2 deletions packages/material-ui/src/ButtonBase/TouchRipple.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import * as React from 'react';
import { TransitionGroup } from 'react-transition-group';
import { StandardProps } from '..';

export interface TouchRippleProps
extends StandardProps<TransitionGroup.TransitionGroupProps, TouchRippleClassKey> {
export type TouchRippleProps = StandardProps<TransitionGroup.TransitionGroupProps, TouchRippleClassKey> & {
center?: boolean;
}

Expand Down
13 changes: 11 additions & 2 deletions packages/material-ui/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import * as React from 'react';
import { StyledComponentProps } from './styles';
export { StyledComponentProps };

export type AnyComponent<P = any> =
| (new (props: P) => React.Component)
| ((props: P & { children?: React.ReactNode }) => React.ReactElement<P> | null);

export type PropsOf<C extends AnyComponent> =
C extends new (props: infer P) => React.Component ? P :
C extends (props: infer P) => React.ReactElement<any> | null ? P :
never;

/**
* All standard components exposed by `material-ui` are `StyledComponents` with
* certain `classes`, on which one can also set a top-level `className` and inline
Expand Down Expand Up @@ -39,7 +48,7 @@ export interface Color {
*
* @internal
*/
export type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
export type Omit<T, K extends keyof any> = T extends any ? Pick<T, Exclude<keyof T, K>> : never;

/**
* `T extends ConsistentWith<T, U>` means that where `T` has overlapping properties with
Expand All @@ -54,7 +63,7 @@ export type ConsistentWith<T, U> = Pick<U, keyof T & keyof U>;
*
* @internal
*/
export type Overwrite<T, U> = (U extends ConsistentWith<U, T> ? T : Omit<T, keyof U>) & U;
export type Overwrite<T, U> = Omit<T, keyof U> & U;

export namespace PropTypes {
type Alignment = 'inherit' | 'left' | 'center' | 'right' | 'justify';
Expand Down
26 changes: 13 additions & 13 deletions packages/material-ui/src/styles/withStyles.d.ts
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 { ConsistentWith, Overwrite } from '..';
import { AnyComponent, ConsistentWith, Overwrite, Omit } from '..';
import { Theme } from './createMuiTheme';
import * as CSS from 'csstype';
import * as JSS from 'jss';
Expand Down Expand Up @@ -38,13 +38,13 @@ export interface WithStylesOptions<ClassKey extends string = string>

export type ClassNameMap<ClassKey extends string = string> = Record<ClassKey, string>;

export type WithStyles<T extends string | StyleRules | StyleRulesCallback = string> = Partial<
WithTheme
> & {
classes: ClassNameMap<
T extends string
? T
: T extends StyleRulesCallback<infer K> ? K : T extends StyleRules<infer K> ? K : never
export type WithStyles<T extends string | StyleRules | StyleRulesCallback = string, IncludeTheme extends boolean | undefined = undefined> =
(IncludeTheme extends true ? WithTheme : Partial<WithTheme>)
& {
classes: ClassNameMap<
T extends string
? T
: T extends StyleRulesCallback<infer K> ? K : T extends StyleRules<infer K> ? K : never
>;
};

Expand All @@ -53,11 +53,11 @@ export interface StyledComponentProps<ClassKey extends string = string> {
innerRef?: React.Ref<any> | React.RefObject<any>;
}

export default function withStyles<ClassKey extends string>(
export default function withStyles<ClassKey extends string, Options extends WithStylesOptions<ClassKey>>(
style: StyleRulesCallback<ClassKey> | StyleRules<ClassKey>,
options?: WithStylesOptions<ClassKey>,
options?: Options,
): {
<P extends ConsistentWith<P, StyledComponentProps<ClassKey>>>(
component: React.ComponentType<P & WithStyles<ClassKey>>,
): React.ComponentType<Overwrite<P, StyledComponentProps<ClassKey>>>;
<P extends ConsistentWith<P, StyledComponentProps<ClassKey> & Partial<WithTheme>>>(
component: AnyComponent<P & WithStyles<ClassKey, Options['withTheme']>>,
): React.ComponentType<Overwrite<Omit<P, 'theme'>, StyledComponentProps<ClassKey>>>;
};
6 changes: 3 additions & 3 deletions packages/material-ui/src/styles/withTheme.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Theme } from './createMuiTheme';
import { ConsistentWith } from '..';
import { AnyComponent, ConsistentWith, Overwrite } from '..';

export interface WithTheme {
theme: Theme;
innerRef?: React.Ref<any> | React.RefObject<any>;
}

declare const withTheme: () => <P extends ConsistentWith<P, WithTheme>>(
component: React.ComponentType<P & WithTheme>,
) => React.ComponentClass<P>;
component: AnyComponent<P & WithTheme>,
) => React.ComponentType<Overwrite<P, Partial<WithTheme>>>;

export default withTheme;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Breakpoint } from '../styles/createBreakpoints';
import { WithWidthProps } from '../withWidth';
import { WithWidth } from '../withWidth';

export interface WithMobileDialogOptions {
breakpoint: Breakpoint;
Expand All @@ -12,5 +12,5 @@ export interface InjectedProps {
export default function withMobileDialog<P = {}>(
options?: WithMobileDialogOptions,
): (
component: React.ComponentType<P & InjectedProps & Partial<WithWidthProps>>,
) => React.ComponentType<P & Partial<WithWidthProps>>;
component: React.ComponentType<P & InjectedProps & Partial<WithWidth>>,
) => React.ComponentType<P & Partial<WithWidth>>;
10 changes: 5 additions & 5 deletions packages/material-ui/src/withWidth/withWidth.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Breakpoint } from '../styles/createBreakpoints';
import { ConsistentWith, Overwrite } from '..';
import { AnyComponent, ConsistentWith, Overwrite } from '..';

export interface WithWidthOptions {
resizeInterval: number;
}

export interface WithWidthProps {
export interface WithWidth {
width: Breakpoint;
innerRef?: React.Ref<any> | React.RefObject<any>;
}
Expand All @@ -24,6 +24,6 @@ export function isWidthUp(

export default function withWidth(
options?: WithWidthOptions,
): <P extends ConsistentWith<P, WithWidthProps>>(
component: React.ComponentType<P & WithWidthProps>,
) => React.ComponentClass<Overwrite<P, Partial<WithWidthProps>>>;
): <P extends ConsistentWith<P, WithWidth>>(
component: AnyComponent<P & WithWidth>,
) => React.ComponentType<Overwrite<P, Partial<WithWidth>>>;
4 changes: 2 additions & 2 deletions packages/material-ui/src/withWidth/withWidth.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import { Grid } from '..';
import { Theme, createStyles } from '../styles';
import withStyles, { WithStyles } from '../styles/withStyles';
import withWidth, { WithWidthProps } from '../withWidth';
import withWidth, { WithWidth } from '../withWidth';

const styles = (theme: Theme) =>
createStyles({
Expand All @@ -13,7 +13,7 @@ const styles = (theme: Theme) =>
},
});

interface IHelloProps extends WithWidthProps, WithStyles<typeof styles> {
interface IHelloProps extends WithWidth, WithStyles<typeof styles> {
name?: string;
}

Expand Down
26 changes: 26 additions & 0 deletions packages/material-ui/test/typescript/styles.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,32 @@ const AllTheComposition = withTheme()(

<AllTheComposition />;

{
const Foo = withTheme()(class extends React.Component<WithTheme> {
render() {
return null;
}
});

<Foo />
}

declare const themed: boolean;
{
// Test that withTheme: true guarantees the presence of the theme
const Foo = withStyles({}, { withTheme: true })(class extends React.Component<WithTheme> {
render() {
return <div style={{ margin: this.props.theme.spacing.unit }} />;
}
});
<Foo />;

const Bar = withStyles({}, { withTheme: true })(({ theme }) => (
<div style={{ margin: theme.spacing.unit }} />
));
<Bar />;
}

// Can't use withStyles effectively as a decorator in TypeScript
// due to https://github.com/Microsoft/TypeScript/issues/4881
//@withStyles(styles)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ interface Painting {

type ArtProps = Book | Painting;

const DecoratedUnionProps = withStyles(styles)<ArtProps>( // <-- without the type argument, we'd get a compiler error!
const DecoratedUnionProps = withStyles(styles)(
class extends React.Component<ArtProps & WithStyles<typeof styles>> {
render() {
const props = this.props;
Expand Down

0 comments on commit 5a0bf7b

Please sign in to comment.