-
Notifications
You must be signed in to change notification settings - Fork 673
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
Friendlier TypeScript errors. #1002
Conversation
packages/css/src/types.ts
Outdated
} | ||
|
||
interface ColorModesScale extends ColorMode { | ||
type ColorModesScale = ColorMode & { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
modes
doesn't match ColorMode index signature (and it shouldn't), so we can't use an interface here.
Property 'modes' of type '{ [k: string]: ColorMode; } | undefined' is not assignable to string index type 'string | string[] | { [K: string]: string | string[] | ...; } | undefined'.
One can still augment ColorMode
, so we don't lose anything here.
packages/css/test/errors.ts
Outdated
@@ -0,0 +1,52 @@ | |||
import { expecter } from 'ts-snippet' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm feeling kinda bad installing a library with 16 stars, but no other solution I'm aware of (e.g. tsd, dtslint, dts-jest) integrates with existing Jest setup with no additional config like ts-snippet does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh, the author looks to be very active on GitHub - probably safe (and certainly convenient).
| CSSSelectorObject | ||
| VariantProperty | ||
| UseThemeFunction | ||
export type ThemeUIStyleObject = ThemeUICSSObject | ThemeDerivedStyles |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can see I removed CSSSelectorObject
and merged all objects into ThemeUICSSObject
.
The naming is not perfect here.
Instead of CSSSelectorObject
, which was causing these really unfriendly errors on all properties, when you made just one typo, we now have CSSOthersObject
to handle nested style objects.
As a side-effect, I made style objects even more permissive than I aimed to do, because we know accept CSS properties on all strings.
This is a styling library, the types here are to make work smoother and more effective. not enforce correctness.
I suppose it's better to be too permissive, than confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hate this (being too permissive) but you're absolutely right that it should aim to be least confusing in the event of an error. I do feel it should enforce correctness but unless we can have both... errors that make sense are probably more valuable than forbidding properties that will just be ignored anyway. 🤷♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed later today, that this isn't too permissive.
There are variables in CSS 🤦
Arbitrary property names (we can't typecheck the leading dash) with values are 100% legit and useful CSS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, good point. Then I guess we're set! 😄
@@ -436,11 +431,15 @@ export interface ThemeUIExtendedCSSProperties | |||
AliasesCSSProperties, | |||
OverwriteCSSProperties {} | |||
|
|||
export type StylePropertyValue<T> = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any property in a style object is either ResponsiveStyleValue (which includes T), a function which returns such value or a nested style object.
@@ -14,7 +9,7 @@ type StandardCSSProperties = CSS.Properties<number | string> | |||
* | |||
* For more information see: https://styled-system.com/responsive-styles | |||
*/ | |||
export type ResponsiveStyleValue<T> = T | Array<T | null> | |||
export type ResponsiveStyleValue<T> = T | Array<T | null | undefined> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I needed it to make @theme-ui/css strict TS, but it makes sense in general. array[index]
can always be undefined.
@@ -1,8 +1,3 @@ | |||
/** | |||
* Copied and adapted from @types/styled-system__css | |||
* https://github.com/DefinitelyTyped/DefinitelyTyped/blob/028c46f833ffbbb0328a28ae6177923998fcf0cc/types/styled-system__css/index.d.ts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes here are pretty noticeable already, I'd say. We probably shouldn't link to "the source" anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
const value = | ||
typeof styles[key] === 'function' ? styles[key](theme) : styles[key] | ||
for (const k in styles) { | ||
const key = k as keyof typeof styles |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A type of key in X isn't always assignable to keys of type of X, even if we forget about the prototype. This part of TypeScript is a bit inconvenient, but it does help with some bugs.
Explanation by Anders Hejlsberg:
microsoft/TypeScript#12253 (comment)
@@ -284,8 +296,8 @@ export const css = (args: ThemeUIStyleObject = {}) => ( | |||
const transform: any = get(transforms, prop, get) | |||
const value = transform(scale, val, val) | |||
|
|||
if (multiples[prop]) { | |||
const dirs = multiples[prop] | |||
if (prop in multiples) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is a nice example of how sometimes common JS doesn't translate to strict TypeScript 😞
I'd guess the reasoning is: Since we don't know if prop
is a key of multiples
, we shouldn't just try to retrieve it without checking. Getters can have side effects!
function mergeAll<A, B>(a: A, B: B): A & B | ||
function mergeAll<A, B, C>(a: A, B: B, c: C): A & B & C | ||
function mergeAll<A, B, C, D>(a: A, B: B, c: C, d: D): A & B & C & D | ||
function mergeAll<T = Theme>(...args: Partial<T>[]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a few overloads to infer the type without explicit annotation.
} | ||
|
||
export interface UseThemeFunction { | ||
(theme: any): Exclude<ThemeUIStyleObject, UseThemeFunction> | ||
export interface ThemeDerivedStyles { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"use" is already a loaded word in React world. Tell me what you think about the rename.
Another name I considered is ComputedStyle
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Emotion calls it FunctionInterpolation
, but since we don't do CSS syntax in tagged template literals here, I'm not sure it would makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed that UseThemeFunction
is not great. I'm good with both ThemeDerivedStyles
and ComputedStyles
(plural?).
packages/css/src/types.ts
Outdated
export interface UseThemeFunction { | ||
(theme: any): Exclude<ThemeUIStyleObject, UseThemeFunction> | ||
export interface ThemeDerivedStyles { | ||
(theme: Theme): Exclude<ThemeUIStyleObject, ThemeDerivedStyles> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added Theme
type to the argument here.
This is a small breaking change for TypeScript ppl who added extra properties to their theme object and use them in functions.
- I'll draft a TypeScript section in the docs, and describe how to add additional properties to the Theme type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, yeah... I have no idea how common of a use case that is, but as long as they still have a way to do it and it's documented, it should be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no idea how common of a use case that is
It actually affects half of my projects using Theme UI. 😢
This is also breaking (theme: MyExactTheme) => ...
usage, where components depend on exact shape of the theme. This makes sense a lot of sense for blogs, but it's a bug in apps where the theme can be customized by the user.
I have mixed feelings about this change.
Possibly, we could add a generic parameter and describe derived styles as <T extends Theme = Theme>(theme: T) => ThemeUICSSObject
to allow this 🤔
example messy usage of exact theme shape
sx={{
bg: (t: MyTheme) => colorMode === 'dark'
? transparentize(t.colors.modes.dark.primary, 0.2)
: darken(t.styles.form.backgroundColor, 0.6)
}}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly, we could add a generic parameter and describe derived styles as
<T extends Theme = Theme>(theme: T) => ThemeUICSSObject
to allow this
That sounds pretty good...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds pretty good...?
Too good :/ It doesn't work. Just tried it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course. 🤦♂️
import { css, Theme } from '@theme-ui/css' | ||
import React from 'react' | ||
import * as React from 'react' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might help with #1000. I didn't test it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably consider using star import for React everywhere.
96c707d
to
624c001
Compare
624c001
to
120192f
Compare
Thanks! I forgot to remove it 🤦
You gentlemen, are highly misinformed. I have no idea what I'm doing. |
Note: Exact themes. This might be a good idea for a PR one day. Ignore this message for now. // library
declare module 'theme-ui' {
export interface MyThemeOverride { }
export type MyTheme = MyThemeOverride extends { theme: infer T } ? T : {};
export const derivedStyles: (f: (theme: Theme & MyTheme) => unknown) => 'not-implemented'
}
import { Theme } from 'theme-ui';
// userspace
const makeTheme = <T extends Theme>(t: T) => t;
const theme = makeTheme({
colors: {
background: 'white',
text: 'black',
blue: {
light: '#187abf',
dark: '#235a97'
}
}
})
type ExactTheme = typeof theme;
declare module 'theme-ui' {
export interface MyThemeOverride {
theme: ExactTheme
}
}
import { derivedStyles } from 'theme-ui';
// when your theme is not dynamic, knowledge about its exact shape is convenient
derivedStyles(t => t.colors.blue.light);
// example
const lightBlue1 = theme.colors.blue.light;
const genericTheme: Theme = theme;
// multiple errors
const lightBlueError = genericTheme.colors.blue.light |
80f6453
to
c9da2e1
Compare
c9da2e1
to
00ff818
Compare
Okay, this PR is probably finished if the page I added to the docs makes sense (@jxnblk) The TS chapter for the docs is here: https://deploy-preview-1002--theme-ui.netlify.app/guides/typescript/ I'm not sure if it should be in guides or below Migrating. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks excellent, and I also really like the docs page. I think I would move the "Exact theme" explanation further up, as I think we want that to be more common use of theme (vs. using .get()
)?
Thanks! Based on a sample of size two of my own projects (serious statistics), I used exact theme type 3 times.
And I found What's important, I broke following scenario css({ color: (theme: ExactTheme) => theme.colors.blue[300] }) in this PR. It worked previously, because
There's a possible fix few comments above. I'll move ExactTheme example to the top and add this. This should be pretty useful. import { ContextValue } from 'theme-ui';
interface ThemeCtx extends Omit<ContextValue, "theme"> {
theme: ExactTheme;
}
export const useTheme = (useThemeUI as unknown) as () => ThemeCtx; |
Okay, I added exact theme hook example and move it up. I'm not sure if I should explain the motivation for it in the docs. I don't want to make them bloated. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks fantastic to me and will be such a huge improvement for our TS users. @jxnblk any further thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm mostly trusting those of you more familiar with TS here, but left some comments about the docs.
Ultimately (not blocking), I think we should point out the differences between usage in TS and JS throughout the docs where relevant, but think this is a good first step and a way to consolidate TS-related things in a single place
const lightBlue = theme.colors.blue.light | ||
``` | ||
|
||
You can then reexport `useThemeUI` hook with narrowed type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For clarification, is ExactType
only used in this instance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not exactly sure what if I understand you here, but yes?: Theme UI types don't know about user's theme exact shape. Everything will just behave like we're writing a bunch of reusable components for any theme.
This can be improved, I think. We could "tell Theme UI about our theme" like in Overmind: https://overmindjs.org/core/typescript#1-declare-module. I only have a proof of concept of this code in one of the comments here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was mostly for my own clarification, and I made a typo, but it sounds like the ExactTheme
type is only needed when you're using the custom useThemeUI
hook, right? I.e. I wouldn't use that type anywhere else?
This can be improved, I think.
Yeah, totally -- I understand that is outside the scope of this PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it sounds like the ExactTheme type is only needed when you're using the custom useThemeUI hook, right? I.e. I wouldn't use that type anywhere else?
ATM that's correct.
I just made #1090 so we can "turn on" using this type everywhere else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Problem
Hey @wKovacs64 @sbardian, would you have some time to take a look at packages/css/src/types.ts?
Described changes and todos in comments 👇