Skip to content

Commit

Permalink
[styles] Overload function signature instead of conditional (#19320)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon authored Jan 21, 2020
1 parent 0d56ff2 commit 784f5fc
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 164 deletions.
36 changes: 8 additions & 28 deletions packages/material-ui-styles/src/makeStyles/makeStyles.d.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,27 @@
import {
ClassKeyOfStyles,
ClassNameMap,
PropsOfStyles,
Styles,
WithStylesOptions,
} from '@material-ui/styles/withStyles';
import { Omit, IsAny, Or, IsEmptyInterface } from '@material-ui/types';
import { Omit } from '@material-ui/types';
import { DefaultTheme } from '../defaultTheme';

/**
* @internal
*
* If a style callback is given with `theme => stylesOfTheme` then typescript
* infers `Props` to `any`.
* If a static object is given with { ...members } then typescript infers `Props`
* to `{}`.
*
* So we require no props in `useStyles` if `Props` in `makeStyles(styles)` is
* inferred to either `any` or `{}`
* `makeStyles` where the passed `styles` do not depend on props
*/
export type StylesRequireProps<S> = Or<
IsAny<PropsOfStyles<S>>,
IsEmptyInterface<PropsOfStyles<S>>
> extends true
? false
: true;

export default function makeStyles<Theme = DefaultTheme, ClassKey extends string = string>(
style: Styles<Theme, {}, ClassKey>,
options?: Omit<WithStylesOptions<Theme>, 'withTheme'>,
): (props?: any) => ClassNameMap<ClassKey>;
/**
* @internal
*
* `Props` are `any` either by explicit annotation or if there are no callbacks
* from which the typechecker could infer a type so it falls back to `any`.
* See the test cases for examples and implications of explicit `any` annotation
* `makeStyles` where the passed `styles` do depend on props
*/
export type StylesHook<S extends Styles<any, any>> = StylesRequireProps<S> extends false
? (props?: any) => ClassNameMap<ClassKeyOfStyles<S>>
: (props: PropsOfStyles<S>) => ClassNameMap<ClassKeyOfStyles<S>>;

export default function makeStyles<
Theme = DefaultTheme,
Props extends {} = {},
ClassKey extends string = string
>(
styles: Styles<Theme, Props, ClassKey>,
options?: Omit<WithStylesOptions<Theme>, 'withTheme'>,
): StylesHook<Styles<Theme, Props, ClassKey>>;
): (props: Props) => ClassNameMap<ClassKey>;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as React from 'react';
import { Theme } from '@material-ui/core';
import { AppBarProps } from '@material-ui/core/AppBar';
import { createStyles, makeStyles } from '@material-ui/styles';
import styled, { StyledProps } from '@material-ui/styles/styled';

// makeStyles
{
Expand Down Expand Up @@ -136,50 +135,3 @@ import styled, { StyledProps } from '@material-ui/styles/styled';
classes.other;
}
}

// styled
{
const StyledButton = styled('button')({
background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
borderRadius: 3,
border: 0,
color: 'white',
height: 48,
padding: '0 30px',
boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
});
const renderedStyledButton = <StyledButton classes={{ root: 'additional-root-class' }} />;
// $ExpectError
const nonExistingClassKey = <StyledButton classes={{ notRoot: 'additional-root-class' }} />;

interface MyTheme {
fontFamily: string;
}
const MyThemeInstance: MyTheme = {
fontFamily: 'monospace',
};
// tslint:disable-next-line: no-empty-interface
interface MyComponentProps extends StyledProps {
defaulted: string;
}
class MyComponent extends React.Component<MyComponentProps> {
static defaultProps = {
defaulted: 'Hello, World!',
};
render() {
const { className, defaulted } = this.props;
return <div className={className}>Greeted?: {defaulted.startsWith('Hello')}</div>;
}
}
const StyledMyComponent = styled<typeof MyComponent>(MyComponent)(
({ theme }: { theme: MyTheme }) => ({
fontFamily: theme.fontFamily,
}),
);
const renderedMyComponent = (
<React.Fragment>
<MyComponent className="test" />
<StyledMyComponent theme={MyThemeInstance} />
</React.Fragment>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { styled } from '@material-ui/styles';
import { styled, StyledProps } from '@material-ui/styles';

function styledTest() {
function themeTest() {
const style = (props: { value: number }) => ({});
const styleWithTheme = (props: {
value: number;
Expand Down Expand Up @@ -46,3 +46,49 @@ function styledTest() {
// error: property 'zIndex' is missing in type ...
<ComponentWithOptionalThemeStyledWithTheme value={1} theme={{ palette: { primary: '#333' } }} />; // $ExpectError
}

function acceptanceTest() {
const StyledButton = styled('button')({
background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
borderRadius: 3,
border: 0,
color: 'white',
height: 48,
padding: '0 30px',
boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
});
const renderedStyledButton = <StyledButton classes={{ root: 'additional-root-class' }} />;
// $ExpectError
const nonExistingClassKey = <StyledButton classes={{ notRoot: 'additional-root-class' }} />;

interface MyTheme {
fontFamily: string;
}
const MyThemeInstance: MyTheme = {
fontFamily: 'monospace',
};
// tslint:disable-next-line: no-empty-interface
interface MyComponentProps extends StyledProps {
defaulted: string;
}
class MyComponent extends React.Component<MyComponentProps> {
static defaultProps = {
defaulted: 'Hello, World!',
};
render() {
const { className, defaulted } = this.props;
return <div className={className}>Greeted?: {defaulted.startsWith('Hello')}</div>;
}
}
const StyledMyComponent = styled<typeof MyComponent>(MyComponent)(
({ theme }: { theme: MyTheme }) => ({
fontFamily: theme.fontFamily,
}),
);
const renderedMyComponent = (
<React.Fragment>
<MyComponent className="test" />
<StyledMyComponent theme={MyThemeInstance} />
</React.Fragment>
);
}
42 changes: 0 additions & 42 deletions packages/material-ui-styles/test/IsEmptyInterface.spec.ts

This file was deleted.

38 changes: 0 additions & 38 deletions packages/material-ui-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,41 +44,3 @@ export type Omit<T, K extends keyof any> = T extends any ? Pick<T, Exclude<keyof
* @internal
*/
export type Overwrite<T, U> = Omit<T, keyof U> & U;

/**
* Returns true if T is any, otherwise false
*/
// https://stackoverflow.com/a/49928360/3406963 without generic branch types
export type IsAny<T> = 0 extends (1 & T) ? true : false;

export type Or<A, B, C = false> = A extends true
? true
: B extends true
? true
: C extends true
? true
: false;

export type And<A, B, C = true> = A extends true
? B extends true
? C extends true
? true
: false
: false
: false;

/**
* @internal
*
* check if a type is `{}`
*
* 1. false if the given type has any members
* 2. false if the type is `object` which is the only other type with no members
* {} is a top type so e.g. `string extends {}` but not `string extends object`
* 3. false if the given type is `unknown`
*/
export type IsEmptyInterface<T> = And<
keyof T extends never ? true : false,
string extends T ? true : false,
unknown extends T ? false : true
>;
2 changes: 1 addition & 1 deletion packages/material-ui/src/styles/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export {
} from './createPalette';
export { default as createStyles } from './createStyles';
export { TypographyStyle } from './createTypography';
export { default as makeStyles, StylesHook } from './makeStyles';
export { default as makeStyles } from './makeStyles';
export { default as responsiveFontSizes } from './responsiveFontSizes';
export { ComponentsPropsList } from './props';
export * from './transitions';
Expand Down
18 changes: 13 additions & 5 deletions packages/material-ui/src/styles/makeStyles.d.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { Theme as DefaultTheme } from './createMuiTheme';
import { Styles, WithStylesOptions } from '@material-ui/styles/withStyles';
import { StylesHook } from '@material-ui/styles/makeStyles';
import { ClassNameMap, Styles, WithStylesOptions } from '@material-ui/styles/withStyles';

import { Omit } from '@material-ui/types';

/**
* `makeStyles` where the passed `styles` do not depend on props
*/
export default function makeStyles<Theme = DefaultTheme, ClassKey extends string = string>(
style: Styles<Theme, {}, ClassKey>,
options?: Omit<WithStylesOptions<Theme>, 'withTheme'>,
): (props?: any) => ClassNameMap<ClassKey>;
/**
* `makeStyles` where the passed `styles` do depend on props
*/
export default function makeStyles<
Theme = DefaultTheme,
Props extends {} = {},
ClassKey extends string = string
>(
styles: Styles<Theme, Props, ClassKey>,
options?: Omit<WithStylesOptions<Theme>, 'withTheme'>,
): StylesHook<Styles<Theme, Props, ClassKey>>;

export { StylesHook };
): (props: Props) => ClassNameMap<ClassKey>;

0 comments on commit 784f5fc

Please sign in to comment.