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

Refine style types #1554

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"mocha": "^3.2.0",
"npmlog": "^4.1.2",
"pre-commit": "^1.1.3",
"prettier": "^1.13.5",
"prettier": "^2.4.0",
"raf": "^3.4.0",
"react": "^16.8.6",
"react-test-renderer": "^16.8.6",
Expand All @@ -98,7 +98,7 @@
"rollup-plugin-terser": "^7.0.2",
"shelljs": "^0.8.2",
"sinon": "4.5.0",
"typescript": "^3.7.0",
"typescript": "^4.4.2",
"webpack": "^4.28.3",
"zen-observable": "^0.6.0"
}
Expand Down
63 changes: 32 additions & 31 deletions packages/jss/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,50 @@
import {Properties as CSSProperties} from 'csstype'
import {Properties as CSSProperties, PropertiesHyphen as CSSPropertiesHyphen} from 'csstype'

// Observable support is included as a plugin. Including it here allows
// TypeScript users to use Observables, which could be confusing if a user
// hasn't installed that plugin.
//
// TODO: refactor to only include Observable types if plugin is installed.
export interface MinimalObservable<T> {
subscribe(
nextOrObserver: ((value: T) => void) | {next: (value: T) => void}
): {unsubscribe: () => void}
subscribe(nextOrObserver: ((value: T) => void) | {next: (value: T) => void}): {
unsubscribe: () => void
}
}

type Func<P, T, R> = T extends undefined ? ((data: P) => R) : ((data: P & {theme: T}) => R)
type Func<Data, Theme, Style> = Theme extends undefined
? (data: Data) => Style | null | undefined
: (data: Data & {theme: Theme}) => Style | null | undefined

type NormalCssProperties = CSSProperties<string | number>
type NormalCssProperties = CSSProperties<string | number> & CSSPropertiesHyphen<string | number>
type NormalCssValues<K> = K extends keyof NormalCssProperties ? NormalCssProperties[K] : JssValue

export type JssStyle<Props = any, Theme = undefined> =
| {
[K in keyof NormalCssProperties]:
| NormalCssValues<K>
| JssStyle<Props, Theme>
| Func<Props, Theme, NormalCssValues<K> | JssStyle<undefined, undefined> | undefined>
| MinimalObservable<NormalCssValues<K> | JssStyle | undefined>
}
| {
[K: string]:
| JssValue
| JssStyle<Props, Theme>
| Func<Props, Theme, JssValue | JssStyle<undefined, undefined> | undefined>
| MinimalObservable<JssValue | JssStyle | undefined>
}

export type JssStyle<Data = any, Theme = undefined> = {
[Key in keyof NormalCssProperties]:
| NormalCssValues<Key>
| JssStyle<Data, Theme>
| Func<Data, Theme, NormalCssValues<Key> | undefined>
| MinimalObservable<NormalCssValues<Key> | JssStyle | undefined>
| null
| undefined
} & // nesting
{
[Key: `${string}&${string}`]: JssStyle<Data, Theme> | null | undefined
}
export type Styles<
Name extends string | number | symbol = string,
Props = unknown,
Data = unknown,
Copy link
Member Author

Choose a reason for hiding this comment

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

This was supposed to be Data all along, the fact it can be Props is up to user, it could be anything

Theme = undefined
> = Record<
Name,
| JssStyle<Props, Theme>
| Array<JssStyle<Props, Theme>>
| string
| Func<Props, Theme, JssStyle<undefined, undefined> | string | null | undefined>
| MinimalObservable<JssStyle | string | null | undefined>
>
> =
| Record<
Name,
| JssStyle<Data, Theme>
| Array<JssStyle<Data, Theme>>
| string
| Func<Data, Theme, JssStyle<undefined, undefined> | string | null | undefined>
| MinimalObservable<JssStyle | string | null | undefined>
>
| Record<`@keyframes ${string}`, Record<'from' | 'to' | `${number}%`, JssStyle<Data, Theme>>>
Copy link
Member

Choose a reason for hiding this comment

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

Is need to add @global here?


export type Classes<Name extends string | number | symbol = string> = Record<Name, string>
export type Keyframes<Name extends string = string> = Record<Name, string>

Expand Down
28 changes: 10 additions & 18 deletions packages/jss/tests/types/Styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const styles: Styles<string, Props> = {
textAlign: 'center',
display: 'flex',
width: '100%',
justifyContent: props => (props.flag ? 'center' : undefined)
justifyContent: (props) => (props.flag ? 'center' : undefined)
},
inner: {
textAlign: 'center',
Expand All @@ -37,7 +37,7 @@ const styles: Styles<string, Props> = {
fontSize: 12
}
},
func: props => ({
func: (props) => ({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
Expand All @@ -47,8 +47,8 @@ const styles: Styles<string, Props> = {
position: 'relative',
pointerEvents: props.flag ? 'none' : null
}),
funcNull: props => null,
funcWithTerm: props => ({
funcNull: (props) => null,
funcWithTerm: (props) => ({
width: props.flag ? 377 : 272,
height: props.flag ? 330 : 400,
boxShadow: '0px 2px 20px rgba(0, 0, 0, 0.08)',
Expand All @@ -68,27 +68,19 @@ const styles: Styles<string, Props> = {
},
'@keyframes fadeIn': {
from: {opacity: 0},
to: {opacity: 1}
to: {opacity: 1},
'50%': {opacity: 0.5}
}
}

// Test supplied Props and Theme
// Verify that nested parameter declarations are banned
const stylesPropsAndTheme: Styles<string, Props, Theme> = {
rootParamDeclaration: ({flag, theme}) => ({
// @ts-expect-error Did you mean to call this expression?
rootParamDeclaration: ({flag, theme}: Props & {theme: Theme}) => ({
fontWeight: 'bold',
// @ts-expect-error
nothingAllowed: ({flag, theme}) => ''
}),
anotherClass: {
color: 'red',
innerParamDeclaration1: ({flag, theme}) => '',
innerParamDeclaration2: ({flag, theme}) => ({
backgroundColor: 'blue',
// @ts-expect-error
nothingAllowed: ({flag, theme}) => ''
})
}
color: ({flag, theme}: Props & {theme: Theme}) => 'red'
})
}

// Test the className types
Expand Down
15 changes: 6 additions & 9 deletions packages/react-jss/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ interface WithStylesProps<
/**
* @deprecated Please use `WithStylesProps` instead
*/
type WithStyles<
S extends Styles<any, any, any> | ((theme: any) => Styles<any, any, undefined>)
> = WithStylesProps<S>
type WithStyles<S extends Styles<any, any, any> | ((theme: any) => Styles<any, any, undefined>)> =
WithStylesProps<S>

declare global {
namespace Jss {
Expand All @@ -76,10 +75,10 @@ interface CreateUseStylesOptions<Theme = DefaultTheme> extends BaseOptions<Theme
name?: string
}

declare function createUseStyles<C extends string = string, Props = unknown, Theme = DefaultTheme>(
styles: Styles<C, Props, Theme> | ((theme: Theme) => Styles<C, Props, undefined>),
declare function createUseStyles<C extends string = string, Data = unknown, Theme = DefaultTheme>(
styles: Styles<C, Data, Theme> | ((theme: Theme) => Styles<C, Data, Theme>),
options?: CreateUseStylesOptions<Theme>
): (data?: Props & {theme?: Theme}) => Classes<C>
): (data?: Data & {theme?: Theme}) => Classes<C>

type GetProps<C> = C extends ComponentType<infer P> ? P : never

Expand All @@ -88,9 +87,7 @@ declare function withStyles<ClassNames extends string | number | symbol, Props,
| Styles<ClassNames, Props, Theme>
| ((theme: Theme) => Styles<ClassNames, Props, undefined>),
options?: WithStylesOptions
): <C>(
comp: C
) => ComponentType<
): <C>(comp: C) => ComponentType<
JSX.LibraryManagedAttributes<
C,
Omit<GetProps<C>, 'classes'> & {
Expand Down
66 changes: 13 additions & 53 deletions packages/react-jss/tests/types/createUseStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const expectedCustomTheme: MyTheme = {color: 'red'}

/* -------------------- THEME ARGUMENT -------------------- */
// Regular, static styles work fine
const themeArg1 = createUseStyles(theme => ({
const themeArg1 = createUseStyles((theme) => ({
someClassName: '',
anotherClassName: {
fontWeight: 'bold'
Expand All @@ -26,20 +26,20 @@ const themeArg1ClassesPass = themeArg1()

// Theme type assumed to be the default
// Nested theme declaration banned
// @ts-expect-error
const themeArg2 = createUseStyles(theme => ({
themeNotAllowed: ({theme: innerTheme}) => ({
fontWeight: 'bold'
const themeArg2 = createUseStyles((parentTheme) => ({
themeNotAllowed: ({theme}: {theme: MyTheme}) => ({
color: theme.color
})
}))
// @ts-expect-error
const themeArg2ClassesFail = themeArg2({theme: {}})
// @ts-expect-error
const themeArg2ClassesFail2 = themeArg2({theme: expectedCustomTheme})
const themeArg2ClassesPass = themeArg2({theme: expectedDefaultTheme})
// @ts-expect-error
const themeArg2ClassesFail2 = themeArg2({theme: expectedDefaultTheme})

// Props declaration is allowed
const themeArg3 = createUseStyles<string, MyProps>(theme => ({
const themeArg3 = createUseStyles<string, MyProps>((theme) => ({
onlyPropsAllowed: ({...props}) => ({
fontWeight: 'bold'
})
Expand All @@ -52,16 +52,15 @@ const themeArg3ClassesPass = themeArg3(expectedCustomProps)
const themeArg3ClassesPass2 = themeArg3({...expectedCustomProps, theme: expectedDefaultTheme})

// Nested props declaration banned
const themeArg4 = createUseStyles<string, MyProps>(theme => ({
const themeArg4 = createUseStyles<string, MyProps>((theme) => ({
onlyPropsAllowed: ({...props}) => ({
fontWeight: 'bold',
// @ts-expect-error
propsNotAllowed: ({...innerProps}) => ''
})
}))

// Supplied theme type is acknowledged
const themeArg5 = createUseStyles<string, unknown, MyTheme>(theme => ({}))
const themeArg5 = createUseStyles<string, unknown, MyTheme>((theme) => ({}))
// @ts-expect-error
const themeArg5ClassesFail = themeArg5({theme: {}})
// @ts-expect-error
Expand All @@ -81,7 +80,7 @@ const themeArg6ClassesFail2 = themeArg6({theme: expectedDefaultTheme})
const themeArg6ClassesPass = themeArg6({theme: expectedCustomTheme})

// Props can be determined implicitly
const themeArg7 = createUseStyles(theme => ({
const themeArg7 = createUseStyles((theme) => ({
checkbox: ({property}: MyProps) => ({
borderColor: property
})
Expand Down Expand Up @@ -172,53 +171,14 @@ const noThemeArg4ClassesPass2 = noThemeArg4({...expectedCustomProps, theme: expe
const noThemeArg5 = createUseStyles<string, MyProps, MyTheme>({
singleNest: {
fontWeight: 'bold',
singleValue: ({property, theme}) => '',
nestOne: ({property, theme}) => ({
// @ts-expect-error
nestOne: ({property, theme}: MyProps & {theme: MyTheme}) => ({
color: 'red',
// @ts-expect-error
nothingAllowed: ({theme: innerTheme, ...innerProps}) => ''
display: () => 'block'
})
}
})

// Nested declarations are banned (double nest test)
const noThemeArg6 = createUseStyles<string, MyProps, MyTheme>({
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't clearly understand what this is testing, because nesting is not generally disallowed, its just only working with nested syntax with &

doubleNest: {
fontWeight: 'bold',
singleValue: ({property, theme}) => '',
firstNest: {
color: 'red',
innerSingleValue: ({property, theme}) => '',
secondNest: ({property, theme}) => ({
backgroundColor: 'blue',
// @ts-expect-error
nothingAllowed: ({theme: innerTheme, ...innerProps}) => ''
})
}
}
})

// Nested declarations are banned (triple nest test)
Copy link
Member Author

Choose a reason for hiding this comment

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

why would one need double nest and triple nest test, is there code that is explicitly working in one of them and not in another?

Copy link
Member

Choose a reason for hiding this comment

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

I know merging of some commits happened earlier. Not sure if this PR holds any of those.

My comments on those kinds of tests are probably misleading. What it should say is probably something like

Nested function declarations are banned

In terms of function declarations, no, I'm not aware of any existing code that tries to nest function declarations. But I thought that I read on the docs somewhere (or in a console warning) that nested functions were not supported. This test was just to ensure that the types would also error out if someone tried to create a nested function (before the runtime bites them).

I did "double" and "triple" failure tests for function declarations just to make sure the type was appropriately defined. This was for two main reasons:

  1. If a person was doing regular nesting that didn't involve functions, they should be able to define a stand-alone, un-nested function wherever they please.
  2. If a person tried to be sneaky and say, "Maybe I can nest function declarations if I start at a deeper point in the nest", this test ensures that they'd be prevented from doing so.

Theoretically speaking, a person could nest as much as they want. But testing every Nth scenario would be impractical. I stopped at 2nd and 3rd level nesting to verify that our recursive types were properly defined.

Copy link
Member

Choose a reason for hiding this comment

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

This probably also answers #1554 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

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

The confusing part was triple test, if we just forbid double nest its already solves the problem

Copy link
Member

Choose a reason for hiding this comment

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

Okay. If you think the double nest is sufficient, I think we can remove all the triple-nest tests. Makes the file shorter 😅

Copy link
Member Author

Choose a reason for hiding this comment

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

I mean if double nesting is already prevented, x nesting is also prevented. It was just confusing to see 3x level of nesting because I start thinking why would you need to test a 3rd level, whats special about it

const noThemeArg7 = createUseStyles<string, MyProps, MyTheme>({
tripleNest: {
fontWeight: 'bold',
singleValue: ({property, theme}) => '',
firstNest: {
color: 'red',
innerSingleValue: ({property, theme}) => '',
secondNest: {
backgroundColor: 'blue',
innerMostSingleValue: ({property, theme}) => '',
thirdNest: ({property, theme}) => ({
display: 'block',
// @ts-expect-error
nothingAllowed: ({theme: innerMostTheme, ...innerMostProps}) => ''
})
}
}
}
})

// Props can be determined implicitly
const noThemeArg8 = createUseStyles({
checkbox: ({property}: MyProps) => ({
Expand Down
6 changes: 3 additions & 3 deletions packages/react-jss/tests/types/docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ type RuleNames = 'myButton' | 'myLabel'
interface ButtonProps {
children?: React.ReactNode
spacing?: number
fontWeight?: string
labelColor?: string
fontWeight?: 'bold'
labelColor?: 'red'
}

interface CustomTheme {
background: string
background: 'gray'
}

const useStyles = createUseStyles<RuleNames, ButtonProps, CustomTheme>({
Expand Down
26 changes: 8 additions & 18 deletions packages/react-jss/tests/types/withStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function functionPlainObject(theme: MyTheme) {
anotherClassName: {
fontWeight: 'bold'
}
}
} as const
}
ResultingComponent = withStyles(functionPlainObject)(SimpleComponent)
ComponentTest = () => <ResultingComponent property="" />
Expand Down Expand Up @@ -80,7 +80,7 @@ const plainObject = {
anotherClassName: {
fontWeight: 'bold'
}
}
} as const
ResultingComponent = withStyles(plainObject)(SimpleComponent)
ComponentTest = () => <ResultingComponent property="" />

Expand Down Expand Up @@ -136,17 +136,7 @@ ComponentTest = () => <ResultingComponent property="" />

/* -------------------- Failing Cases -------------------- */

// A function argument cannot provide another defined theme type conflicting with `undefined`
function failingFunctionRedefineTheme(theme: MyTheme): Styles<string, unknown, any> {
return {
someClassName: '',
anotherClassName: {
fontWeight: 'bold'
}
}
}

function passingFunctionUnknownTheme(theme: MyTheme): Styles<string, unknown, unknown> {
function failingFunctionWrongTheme(theme: MyTheme): Styles<string, unknown, MyTheme> {
return {
someClassName: '',
anotherClassName: {
Expand All @@ -155,7 +145,7 @@ function passingFunctionUnknownTheme(theme: MyTheme): Styles<string, unknown, un
}
}

function passingFunctionNullTheme(theme: MyTheme): Styles<string, unknown, null> {
function failingFunctionNullTheme(theme: MyTheme): Styles<string, unknown, null> {
return {
someClassName: '',
anotherClassName: {
Expand All @@ -164,7 +154,7 @@ function passingFunctionNullTheme(theme: MyTheme): Styles<string, unknown, null>
}
}

// @ts-expect-error
withStyles(failingFunctionRedefineTheme)(SimpleComponent)
withStyles(passingFunctionUnknownTheme)(SimpleComponent)
withStyles(passingFunctionNullTheme)(SimpleComponent)
// @ts-expect-error - can't use a wrong theme
withStyles(failingFunctionWrongTheme)(SimpleComponent)
// @ts-expect-error - can't use null as a theme
withStyles(failingFunctionNullTheme)(SimpleComponent)
Loading