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.
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.
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.
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.
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.
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.
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.
Oh, good point. Then I guess we're set! 😄
| AliasesCSSProperties, | ||
| OverwriteCSSProperties {} | ||
|
|
||
| export type StylePropertyValue<T> = |
There was a problem hiding this comment.
Any property in a style object is either ResponsiveStyleValue (which includes T), a function which returns such value or a nested style object.
| * 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.
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.
The changes here are pretty noticeable already, I'd say. We probably shouldn't link to "the source" anymore.
| 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.
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)
|
|
||
| if (multiples[prop]) { | ||
| const dirs = multiples[prop] | ||
| if (prop in multiples) { |
There was a problem hiding this comment.
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.
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.
"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.
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.
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.
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.
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.
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.
Possibly, we could add a generic parameter and describe derived styles as
<T extends Theme = Theme>(theme: T) => ThemeUICSSObjectto allow this
That sounds pretty good...?
There was a problem hiding this comment.
That sounds pretty good...?
Too good :/ It doesn't work. Just tried it.
| // @ts-ignore | ||
| import { css, Theme } from '@theme-ui/css' | ||
| import React from 'react' | ||
| import * as React from 'react' |
There was a problem hiding this comment.
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. |
mxstbr
left a comment
There was a problem hiding this comment.
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. |
jxnblk
left a comment
There was a problem hiding this comment.
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.
For clarification, is ExactType only used in this instance?
There was a problem hiding this comment.
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.
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.
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.
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 👇