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

Friendlier TypeScript errors. #1002

Merged
merged 23 commits into from
Jun 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7870a45
Add label prop to ThemeUIStyleObject
hasparus Jun 14, 2020
0647c3c
Change core/test/index extension to tsx
hasparus Jun 14, 2020
8e65bd9
Add overloads to merge.all
hasparus Jun 14, 2020
41e2e63
Fix type errors in core/test/index
hasparus Jun 14, 2020
671c4e7
Bump deps to use optional chaining in tests
hasparus Jun 14, 2020
089b302
Use optional chaining in @theme-ui/css tests
hasparus Jun 14, 2020
103d6b9
Report friendlier TypeScript errors in style objects
hasparus Jun 15, 2020
ea8346f
Make @theme-ui/css strict TypeScript
hasparus Jun 15, 2020
8cfdfad
Test that ThemeUIStyleObject errors are not confusing anymore
hasparus Jun 15, 2020
bebb666
Update reexport name
hasparus Jun 15, 2020
120192f
Import * as React in @theme-ui/core
hasparus Jun 15, 2020
8722aab
Remove unused export
hasparus Jun 15, 2020
30c522f
Make test/errors.ts expectSnippet strict
hasparus Jun 17, 2020
89253aa
Export ColorModesScale
hasparus Jun 23, 2020
8cfd2ef
Draft TypeScript chapter for the docs
hasparus Jun 23, 2020
00ff818
Add numeric index signature to ObjectOrArray
hasparus Jun 23, 2020
a5ad143
Remove unused imports
hasparus Jun 23, 2020
9033499
Add exact theme reexport example
hasparus Jun 25, 2020
3836ea1
Reference other pages in docs and add a sentence
hasparus Jun 28, 2020
92b13ec
Merge remote-tracking branch 'origin/master' into better-ts-errors
hasparus Jun 28, 2020
26017d4
Rename ObjectOrArray to Scale
hasparus Jun 28, 2020
b4a883f
Steal jxnblk's paragraph
hasparus Jun 30, 2020
0756514
Update typescript.mdx
hasparus Jun 30, 2020
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
29 changes: 16 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,38 +16,41 @@
"packages/*"
],
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.5",
"@babel/helper-validator-identifier": "^7.9.0",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.4.5",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.8.3",
"@babel/runtime": "^7.7.7",
"@babel/cli": "^7.10.1",
"@babel/core": "^7.10.2",
"@babel/helper-validator-identifier": "^7.10.1",
"@babel/plugin-transform-runtime": "^7.10.1",
"@babel/preset-env": "^7.10.2",
"@babel/preset-react": "^7.10.1",
"@babel/preset-typescript": "^7.10.1",
"@babel/runtime": "^7.10.2",
"@testing-library/react": "^9.1.3",
"@types/jest": "^25.1.2",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.merge": "^4.6.6",
"@types/react-test-renderer": "^16.9.2",
"babel-jest": "^25.3.0",
"babel-jest": "^26.0.1",
"husky": ">=4.0.7",
"jest": "^24.8.0",
"jest": "^26.0.1",
"jest-canvas-mock": "^2.2.0",
"jest-emotion": "^10.0.11",
"jest-mock-console": "^1.0.0",
"jest-emotion": "^10.0.32",
"jest-mock-console": "^1.0.1",
"lerna": "^3.14.1",
"lint-staged": "10",
"microbundle": "^0.11.0",
"prettier": "^2.0.5",
"react-test-renderer": "^16.8.6",
"ts-jest": "^25.2.0"
"ts-jest": "^26.1.0",
"ts-snippet": "^4.2.0",
"typescript": "^3.9.5"
},
"resolutions": {},
"jest": {
"preset": "ts-jest/presets/js-with-babel",
"globals": {
"ts-jest": {
"tsConfig": {
"target": "es2018",
"module": "commonjs",
"esModuleInterop": true,
"resolveJsonModule": true,
Expand Down
15 changes: 10 additions & 5 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import {
ThemeContext as EmotionContext,
InterpolationWithTheme,
} from '@emotion/core'
// @ts-ignore
import { css, Theme } from '@theme-ui/css'
import React from 'react'
import * as React from 'react'
Copy link
Member Author

Choose a reason for hiding this comment

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

See facebook/react#18102

It might help with #1000. I didn't test it.

Copy link
Member Author

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.

import deepmerge from 'deepmerge'
import { version as __EMOTION_VERSION__ } from '@emotion/core/package.json'

Expand Down Expand Up @@ -72,8 +71,14 @@ const arrayMerge = (destinationArray, sourceArray, options) => sourceArray
export const merge = (a: Theme, b: Theme): Theme =>
deepmerge(a, b, { isMergeableObject, arrayMerge })

merge.all = <T = Theme>(...args: Partial<T>[]) =>
deepmerge.all<T>(args, { isMergeableObject, arrayMerge })
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>[]) {
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 added a few overloads to infer the type without explicit annotation.

return deepmerge.all<T>(args, { isMergeableObject, arrayMerge })
}

merge.all = mergeAll

interface BaseProviderProps {
context: ContextValue
Expand Down Expand Up @@ -109,7 +114,7 @@ export function ThemeProvider({ theme, children }: ThemeProviderProps) {
const context =
typeof theme === 'function'
? { ...outer, theme: theme(outer.theme) }
: merge.all<ContextValue>({}, outer, { theme })
: merge.all({}, outer, { theme })

return jsx(BaseProvider, { context }, children)
}
48 changes: 30 additions & 18 deletions packages/core/test/index.js → packages/core/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ import renderer from 'react-test-renderer'
import { render, fireEvent, cleanup, act } from '@testing-library/react'
import { matchers } from 'jest-emotion'
import mockConsole from 'jest-mock-console'
import { jsx, Context, useThemeUI, merge, ThemeProvider } from '../src'
import {
jsx,
Context,
useThemeUI,
merge,
ThemeProvider,
ContextValue,
} from '../src'

afterEach(cleanup)

expect.extend(matchers)

const renderJSON = (el) => renderer.create(el).toJSON()
const renderJSON = (el: React.ReactElement) => renderer.create(el).toJSON()

describe('ThemeProvider', () => {
test('renders', () => {
Expand All @@ -27,7 +34,8 @@ describe('ThemeProvider', () => {
const json = renderJSON(
<Context.Provider
value={{
emotionVersion: '9.0.0',
__EMOTION_VERSION__: '9.0.0',
theme: {},
}}>
<ThemeProvider theme={{}}>Conflicting versions</ThemeProvider>
</Context.Provider>
Expand Down Expand Up @@ -216,7 +224,7 @@ describe('jsx', () => {
test('does not add css prop when not provided', () => {
jest.spyOn(global.console, 'warn')
const json = renderJSON(jsx(React.Fragment, null, 'hi'))
expect(json.props).toEqual(undefined)
expect(json?.props).toEqual(undefined)
expect(console.warn).not.toBeCalled()
})
})
Expand All @@ -225,6 +233,7 @@ describe('merge', () => {
test('deeply merges objects', () => {
const result = merge(
{
// @ts-ignore
beep: 'boop',
hello: {
hi: 'howdy',
Expand Down Expand Up @@ -267,9 +276,12 @@ describe('merge', () => {
})

test('does not attempt to merge React components', () => {
const h1 = React.forwardRef((props, ref) => <h1 ref={ref} {...props} />)
const h1 = React.forwardRef<HTMLHeadingElement, {}>((props, ref) => (
<h1 ref={ref} {...props} />
))
const result = merge(
{
//@ts-ignore
h1: (props) => <h1 {...props} />,
},
{
Expand All @@ -282,53 +294,53 @@ describe('merge', () => {
test('primitive types override arrays', () => {
const result = merge(
{
fontSize: [3, 4, 5],
fontSizes: [3, 4, 5],
},
{
fontSize: 4,
fontSizes: 4 as any,
}
)
expect(result).toEqual({
fontSize: 4,
fontSizes: 4,
})
})

test('arrays override arrays', () => {
const result = merge(
{
fontSize: [3, 4, 5],
fontSizes: [3, 4, 5],
},
{
fontSize: [6, 7],
fontSizes: [6, 7],
}
)
expect(result).toEqual({
fontSize: [6, 7],
fontSizes: [6, 7],
})
})

test('arrays override primitive types', () => {
const result = merge(
{
fontSize: 5,
fontSizes: 5 as any,
},
{
fontSize: [6, 7],
fontSizes: [6, 7],
}
)
expect(result).toEqual({
fontSize: [6, 7],
fontSizes: [6, 7],
})
})
})

// describe('Context', () => {})
describe('useThemeUI', () => {
test('returns theme context', () => {
let context
const GetContext = (props) => {
let context: ContextValue | undefined
const GetContext = () => {
context = useThemeUI()
return false
return null
}
renderJSON(
<ThemeProvider
Expand All @@ -341,6 +353,6 @@ describe('useThemeUI', () => {
</ThemeProvider>
)
expect(context).toBeTruthy()
expect(context.theme.colors.text).toBe('tomato')
expect(context?.theme.colors?.text).toBe('tomato')
})
})
48 changes: 29 additions & 19 deletions packages/css/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import { CSSObject, ThemeUIStyleObject, UseThemeFunction, Theme } from './types'
import {
CSSObject,
ThemeUIStyleObject,
ThemeDerivedStyles,
Theme,
ThemeUICSSObject,
} from './types'

export * from './types'

export function get(
obj: object,
key: string | number,
key: string | number | undefined,
def?: unknown,
p?: number,
undef?: unknown
): any {
const path = key && typeof key === 'string' ? key.split('.') : [key]
for (p = 0; p < path.length; p++) {
obj = obj ? (obj as any)[path[p]] : undef
obj = obj ? (obj as any)[path[p]!] : undef
}
return obj === undef ? def : obj
}

export const defaultBreakpoints = [40, 52, 64].map(n => n + 'em')
export const defaultBreakpoints = [40, 52, 64].map((n) => n + 'em')

const defaultTheme = {
space: [0, 4, 8, 16, 32, 64, 128, 256, 512],
Expand Down Expand Up @@ -215,20 +221,23 @@ const transforms = [
{}
)

const responsive = (styles: Exclude<ThemeUIStyleObject, UseThemeFunction>) => (
theme?: Theme
) => {
const next: Exclude<ThemeUIStyleObject, UseThemeFunction> = {}
const responsive = (
styles: Exclude<ThemeUIStyleObject, ThemeDerivedStyles>
) => (theme?: Theme) => {
const next: Exclude<ThemeUIStyleObject, ThemeDerivedStyles> = {}
const breakpoints =
(theme && (theme.breakpoints as string[])) || defaultBreakpoints
const mediaQueries = [
null,
...breakpoints.map(n => `@media screen and (min-width: ${n})`),
...breakpoints.map((n) => `@media screen and (min-width: ${n})`),
]

for (const key in styles) {
const value =
typeof styles[key] === 'function' ? styles[key](theme) : styles[key]
for (const k in styles) {
const key = k as keyof typeof styles
Copy link
Member Author

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)

let value = styles[key]
if (typeof value === 'function') {
value = value(theme || {})
}

if (value == null) continue
if (!Array.isArray(value)) {
Expand All @@ -243,7 +252,7 @@ const responsive = (styles: Exclude<ThemeUIStyleObject, UseThemeFunction>) => (
}
next[media] = next[media] || {}
if (value[i] == null) continue
next[media][key] = value[i]
;(next[media] as Record<string, any>)[key] = value[i]
}
}

Expand All @@ -259,22 +268,23 @@ export const css = (args: ThemeUIStyleObject = {}) => (
...defaultTheme,
...('theme' in props ? props.theme : props),
}
let result = {}
let result: CSSObject = {}
const obj = typeof args === 'function' ? args(theme) : args
const styles = responsive(obj)(theme)

for (const key in styles) {
const x = styles[key]
const x = styles[key as keyof typeof styles]
const val = typeof x === 'function' ? x(theme) : x

if (key === 'variant') {
const variant = css(get(theme, val))(theme)
const variant = css(get(theme, val as string))(theme)
result = { ...result, ...variant }
continue
}

if (val && typeof val === 'object') {
result[key] = css(val)(theme)
// TODO: val can also be an array here. Is this a bug? Can it be reproduced?
result[key] = css(val as ThemeUICSSObject)(theme)
continue
}

Expand All @@ -284,8 +294,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) {
Copy link
Member Author

@hasparus hasparus Jun 15, 2020

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!

const dirs = multiples[prop as keyof typeof multiples]

for (let i = 0; i < dirs.length; i++) {
result[dirs[i]] = value
Expand Down
Loading