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

Fix useTheme hook type #23

Closed
wants to merge 1 commit into from

Conversation

alexaragao
Copy link
Contributor

I'm solving my own issue (#22), this changes did work for me in my project.

Copy link
Owner

@Temzasse Temzasse left a comment

Choose a reason for hiding this comment

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

Thanks a lot! 👍🏻

Copy link
Owner

@Temzasse Temzasse left a comment

Choose a reason for hiding this comment

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

I jumped the gun with my review a bit, sorry about that. This change is in the right direction but not quite 100% correct since currently the useTheme hook in Stitches Native actually returns the pure theme token values and not the so called theme definition object that this added type describes.

The reason for this is that I didn't see the benefit of returning the definition object:

{ token, scale, value, toString }

when in most cases you just want to use the pure value when access the theme:

const theme = useTheme();
const x = theme.colors.white; // <---- this is a string not an object
const y = theme.space[1]; // <---- this is a number not an object

@Temzasse
Copy link
Owner

If we changed the useTheme hook to return the so-called theme definition object { token, scale, value, toString } it would create a new problem where all theme values would be strings since the value part of the theme definition object is always a string even for theme.space[1].value if you take a closer look at the ThemeUtil.Token.

@alexaragao
Copy link
Contributor Author

I see. So the root problem is the type of theme. How about use the token value as the value of the theme token? (Kinda confusing).

The theme type would be something like:

// ...
theme: string &
    {
      [Scale in keyof Theme]: {
        [Token in keyof Theme[Scale]]: ThemeUtil.Token<
          Extract<Token, string | number>,
          Theme[Scale][Token],
          Extract<Scale, string | void>
        >;
      };
    };
// ...

That's make the value attribute from the token be the same described in the configuration file. So if a space token is a string (like "100%"), the .value will return a string, and if it is a number (like 300) it will return a number.

@alexaragao
Copy link
Contributor Author

alexaragao commented Mar 18, 2022

I'm checking the original stitches project and noticed that value is always a string. I think this isn't a problem for web because we need to explicit provide a unit for both space (like "100px", "200rem", "300em", etc) and color values (like "#000000", "rgb(0, 0, 0)"). But for react native, while color is always a string (I think so), the space can be a string or a number.

@alexaragao
Copy link
Contributor Author

I've noticed that theme.colors actually returns the color object, not a Record<string, Token>. This behavior is different from default stitches, but I think is wanted according to README.md examples. So what about ignore the whole ThemeUtils.Token type and return theme as:

theme: string &
    {
      [Scale in keyof Theme]: {
        [Token in keyof Theme[Scale]]: Theme[Scale][Token];
      };
    };

@Temzasse
Copy link
Owner

Yeah that definitely mostly solves the problem 👍🏻 Instead of mapping the Theme keys we can actually just return the Theme directly since

{
  [Scale in keyof Theme]: {
    [Token in keyof Theme[Scale]]: Theme[Scale][Token];
  };
};

is exactly the same as just Theme 😄

One remaining problem that this doesn't solve is that token aliases inside the theme will have a string type even though the resolved value might be a number, eg:

space: {
  1: 8, <--- Will have correct `number` type
  alias: '$1', <--- Will have incorrect `string` type
}

But maybe that is a trade-off that we can make to satisfy the most common use case 🤔

@Temzasse
Copy link
Owner

Okay I was able to create a utility type that casts token aliases as unknown while keeping the original type for all the non-alias tokens:

type IsPrefixed<T extends string = ''> = T extends `${infer Head}${infer Tail}`
  ? Head extends '$'
    ? 'true'
    : 'false'
  : 'false';

export type TokenValue<Value extends string | number> = Value extends number
  ? number
  : IsPrefixed<Value> extends 'true'
  ? unknown
  : string;

We can use this type for useTheme:

useTheme: () => {
  [Scale in keyof Theme]: {
    [Token in keyof Theme[Scale]]: ThemeUtil.TokenValue<Theme[Scale][Token]>;
  };
};

In order to get this to work the user needs to assert the token alias as const in the theme definition:

{
  colors: {
    blue100: '#ab9cf7', // <-- this will be `string`
    primary: '$blue100' as const, // <-- this will be `unknown` when accessing from `useTheme`
  },
  space: {
    1: 8,  // <-- this will be `number`
    foo: '$1' as const, // <-- this will be `unknown` when accessing from `useTheme`
    bar: '$1', // <-- this will be incorrectly `string` since `as const` is missing
  }
}

I couldn't get the actual type of the token alias (not sure it's even possible in TS 🤷🏻‍♂️). This did not work:

export type TokenValue<
  Tokens extends {} = {},
  Token extends keyof Tokens,
  Value extends string | number = Tokens[Token]
> = Value extends number
  ? number
  : IsPrefixed<Value> extends 'true'
  ? Tokens[Value]
  : string;

However, I think it is better to have the type to be unknown than incorrectly always be string for all token aliases.

What do you @alexaragao think?

@alexaragao
Copy link
Contributor Author

@Temzasse I do agree that return unknown is much better than return string for token aliases. Particularly, I don't like to have to use as const for token aliases, but, as you said, I also not sure if it's possible to get the actual type of the token. I'll do some tests and research and soon come with an answer. I liked the proposed solution so far!

@alexaragao
Copy link
Contributor Author

alexaragao commented Mar 20, 2022

Maintaining the use of as const for aliases tokens, I got the following proposal:

  • Rewrite your IsPrefixed to not return only if it is or not an alias token, but if so, return the aliased token (I don't like the name I give, but I'm not sure how can it be named):
type AliasedToken<T extends string> = T extends `${infer Head}${infer Tail}`
  ? Head extends "$"
    ? Tail
    : never
  : never;
  • If the token is an alias, return the aliased token type:
theme: string &
    {
      [Scale in keyof Theme]: {
        [Token in keyof Theme[Scale]]:
        // Check if token value is a string
        Theme[Scale][Token] extends string
        ? (
          // Check if token value is an alias, never means 'no'
          AliasedToken<Theme[Scale][Token]> extends never
          // if not, return string
          ? string
          // if so, return token value type
          : Theme[Scale][AliasedToken<Theme[Scale][Token]>]
        )
        // if not, return token value type
        : Theme[Scale][Token];
      };
    };

The type is kinda ugly, but prevents the unknown type. I'll keep looking for a way to prevent the use of as const. What do you @Temzasse think?

EDIT: My solution to prevent empty token aliases (use only "$") had a bug. Now it returns unknown if the aliased token doesn't exists.

@Temzasse
Copy link
Owner

@alexaragao oh wow you managed to extract the actual type for the alias, great job! 🤩 🙌🏻

The second code snippet in your message should be useTheme: xxx not theme: xxx, right? The returned theme object from the createTheme is not the same type of theme as the one returned from useTheme. The theme from createTheme is what I referred earlier as a "theme definition" object where you have the same stuff that the original Stitches returns { token, scale, value, toString }. A user should not use this theme object for reading the actual token values since this object won't change if you change the theme during runtime. I don't think this theme object has much use in React Native but I included it to be more API compatible with web Stitches. The main way to access the theme is via useTheme.

How do you want to proceed with these changes? I could apply your type proposal in the main branch and also write some guidance about the need for as const for token aliases. Once I have published those changes we can close this PR 🙂

PS: I'm quite sure there is no way to avoid the as const since without it TS can't process the $ in the string since the value is any string instead of a literal string.

@alexaragao
Copy link
Contributor Author

alexaragao commented Mar 20, 2022

Thanks 😄! I overwrote my commit with the proposed changes, everything should be fine now. I only changed the type of useTheme as instructed. Jump the gun!

NOTE: I tried to "hard cast" the aliased token using readonly string, but got no success.

@Temzasse
Copy link
Owner

This is now fixed in the v0.1.2 version of Stitches Native.

Big thanks for the fix @alexaragao! 👍🏻

@Temzasse Temzasse closed this Mar 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants