Jss: [react-jss] createUseStyles has strange types in TypeScript

Created on 2 Oct 2019  路  17Comments  路  Source: cssinjs/jss

We (that is @mattwagl, @yeldiRium and me) have recently migrated our module's dependencies jss and react-jss to 10.0.0, and have tried to convert from the HOC approach withStyles function to the hook-based approach with createUseStyles.

Since we are using theming, we would like to use createUseStyles with a function parameter, basically like this:

const useStyles = createUseStyles(theme => ({
  // ...
}));

Problems start when we look at the types of the useStyles function.

__Expected behavior:__

What we expect is that useStyles should return something of type Styles, or at least something such as Record<string, string>.

__Actual behavior:__

What it actually returns is Record<never, string>. We don't know where this never comes from, but obviously this is not what it should be 馃槈

__Workaround__

We are able to fix this by using

const useStyles = createUseStyles<string>(theme => ({
  // ...
}));

but this for sure can't be the right way to go. Actually, it even makes us wonder that this works, since createUseStyles takes two type parameters when providing a function. Something with the typings seems to be broken here (or we missed something very essential with respect to this new hook-based approach).

__System environment__

  • Node.js 12.10.0
  • macOS 10.14.6 (Mojave)
typescript

Most helpful comment

I will leave this here in case people want a temporary typing solution while this is being worked on but this is what I was able to come up with this morning.

I had to override the return type of the default function with a as any but I guess the team at jss can maybe get some inspiration from the following code:

const styleFunction = theme => ({
  image: {
    display: "flex",
    width: "100%"
  }
});

// OR

const styleObject = {
  image: {
    display: "flex",
    width: "100%"
  }
};

function betterCreateUseStyles<TStyle>(
  style: TStyle
): TStyle extends (...args: any) => any
  ? (data?: any) => Record<Extract<keyof ReturnType<TStyle>, string>, string>
  : (data?: any) => Record<Extract<keyof TStyle, string>, string> {
  return createUseStyles(style) as any;
}

/**
 * `(data: any) => { image: string; }`
 */
betterCreateUseStyles(styleFunction);

/**
 * `(data: any) => { image: string; }`
 */
betterCreateUseStyles(styleObject);

EDIT

I just created myself a typings/react-jss.d.ts where I added

declare module "react-jss" {
  export function createUseStyles<TStyle>(
    style: TStyle
  ): TStyle extends (...args: any) => any
    ? (data?: any) => Record<Extract<keyof ReturnType<TStyle>, string>, string>
    : (data?: any) => Record<Extract<keyof TStyle, string>, string>;
}

export * from "react-jss";

All 17 comments

Thanks @goloroden! Maybe we can also provide some help (e.g. add docs about the correct way to use the new API with TypeScript).

Any update on this ?

Seems like we've been using the types incorrectly. We've been studying index.d.ts inside the react-jss module a bit more and came up with a solution that currently works for us. You have to provide two type variables to the createUseStyles call. It seems like the first one should be a Theme and the second one is a list of class names. So our current setup looks something like this:

type ButtonClassNames =  'Button' | 'BackgroundTransparent';

const useStyles = createUseStyles<Theme, ButtonClassNames>((theme: Theme): Styles<ButtonClassNames> => {
  Button: {
  },
  BackgroundTransparent: {
  },
});

We're still not 100% sure if this is the correct way to go. @HenriBeck if there is something we can do to help, please let us know.

@mattwagl You are indeed correct. The first generic parameter is the theme, and the second one are the class names.

There should also be overload with only one generic, which accepts only the class names.

I'm currently reworking these types as well to also include the data which is in turn provided as the first argument to the useStyles.

Does it currently work that Typescript infers the class names properly so you don't need to declare them on the outside? For my liking, your example has too many self typed values, which Typescript should be able to infer correctly. If not, I will have a look why that doesn't work.

@HenriBeck Currently we're declaring the classNames by hand on the outside which is indeed a duplication we'd like to get rid of. But we haven't figured out a way to do this yet. So if this would be possible (with your rewrite) that'd be great. I'm not quite sure if it is possible with TypeScript. Especially when you're using a theme (which we do in nearly every component) and the parameter you pass to the createUseStyles is a fat arrow function that returns a "dynamic" object. Disclaimer: I'm still new to TypeScript. :-)

I will leave this here in case people want a temporary typing solution while this is being worked on but this is what I was able to come up with this morning.

I had to override the return type of the default function with a as any but I guess the team at jss can maybe get some inspiration from the following code:

const styleFunction = theme => ({
  image: {
    display: "flex",
    width: "100%"
  }
});

// OR

const styleObject = {
  image: {
    display: "flex",
    width: "100%"
  }
};

function betterCreateUseStyles<TStyle>(
  style: TStyle
): TStyle extends (...args: any) => any
  ? (data?: any) => Record<Extract<keyof ReturnType<TStyle>, string>, string>
  : (data?: any) => Record<Extract<keyof TStyle, string>, string> {
  return createUseStyles(style) as any;
}

/**
 * `(data: any) => { image: string; }`
 */
betterCreateUseStyles(styleFunction);

/**
 * `(data: any) => { image: string; }`
 */
betterCreateUseStyles(styleObject);

EDIT

I just created myself a typings/react-jss.d.ts where I added

declare module "react-jss" {
  export function createUseStyles<TStyle>(
    style: TStyle
  ): TStyle extends (...args: any) => any
    ? (data?: any) => Record<Extract<keyof ReturnType<TStyle>, string>, string>
    : (data?: any) => Record<Extract<keyof TStyle, string>, string>;
}

export * from "react-jss";

for anyone who created a Theme interface, can use this to use either an object or a function that receives that Theme

declare module 'react-jss' {
  export function createUseStyles<TStyle extends (theme: Theme) => Record<Extract<ReturnType<TStyle>, string>, string>>(
    style: TStyle,
  ): (data?: any) => Record<Extract<keyof ReturnType<TStyle>, string>, string>

  export function createUseStyles<TStyle>(style: TStyle): (data?: any) => Record<Extract<keyof TStyle, string>, string>
}

released in 10.0.3

@jasonsoden Your example did not work for me. I am seeing "Expected 1 type arguments, but got 2"

But I am not sure what that all means because I am new to typescript.

How would you define ITheme? Is there another example of how to use the createUseStyles(theme => ({})) function with typescript?

@jasonsoden Your example did not work for me. I am seeing "Expected 1 type arguments, but got 2"

But I am not sure what that all means because I am new to typescript.

How would you define ITheme? Is there another example of how to use the createUseStyles(theme => ({})) function with typescript?

@bensoutendijk They fixed (broke) lots of types between 10.0.0 and 10.0.3 release (see changes).

With the 10.0.3 release, you must use the following,

const styles = { };
const useStyles = createUseStyles(styles);

or

const styles = (theme: ITheme) => ({ });
const useStyles = createUseStyles<keyof ReturnType<typeof styles>>(styles);

In a future release, I hope to see createThemeStyles(styles) so we don't have to specify the type. Edit: We should make createUseStyles(styles) return a type with correct keys when styles is a function.

@jasonsoden was this supposed to happen in a major release? is this considered a breaking change?

@jasonsoden was this supposed to happen in a major release? is this considered a breaking change?

Here's the changes I discovered:

10.0.0: const useStyles = createUseStyles<ITheme, keyof ReturnType<typeof styles>>(styles);;
10.0.3: const useStyles = createUseStyles<keyof ReturnType<typeof styles>>(styles);
Change: now has 1 generic type instead of 2

10.0.0: type Props = { } & WithStyles<typeof styles>;
10.0.3: type Props = { } & WithStylesProps<typeof styles>;
Change: type renamed

The renaming of WithStyles is the biggest concern since it's a widely used type. createUseStyles is still new I think and has less of an impact.

@jasonsoden was this supposed to happen in a major release? is this considered a breaking change?

@kof Even though it's a global find and replace, renaming WithStyles to WithStylesProps is a breaking change and should be in a major release.

cc @HenriBeck we could revert it in the 10.0.4 release and republish as 11

cc @HenriBeck we could revert it in the 10.0.4 release and republish as 11

I don't think this change deservers a complete major release. We only introduced these types recently here in the main repo.

We could add a type alias and mark that one as deprecated. That would be the correct solution here, in my opinion.

@jasonsoden Regarding the usage with the createUseStyles and the theme, it shouldn't be necessary to type the generic. That should be inferred by Typescript itself.

const styles = (theme: ITheme) => ({ });
const useStyles = createUseStyles(styles);

@HenriBeck It's not related to the theme. If you don't specify the type as keyof ReturnType<typeof styles>, the classes object keys aren't typed. Here's a repro.

image

We typically use the styles function with a theme which is why I mentioned it.

Was this page helpful?
0 / 5 - 0 ratings