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

Updated types to support global Theme definition #1609

Merged
merged 19 commits into from
Dec 7, 2019
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
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
37 changes: 18 additions & 19 deletions docs/typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -248,34 +248,33 @@ const App = () => (
### Define a Theme
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The whole docs on Theme usage with TS feels massively outdated now, and I feel like it should be reworded altogether.

Exposing for discussion.

Copy link
Member

Choose a reason for hiding this comment

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

Right - I feel like we can handle it in a separate PR though. Ideally, such docs would present the current solution with its tradeoffs and incorporate stuff from this #973

Copy link
Contributor

Choose a reason for hiding this comment

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

If it's not in this PR, it should definitely be a blocker for releasing v11.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, this is crucial and I plan to get this done before v11 👍


By default, `props.theme` has an `any` type annotation and works without any errors.
Andarist marked this conversation as resolved.
Show resolved Hide resolved
However, you can define a theme type by creating another `styled` instance.

_styled.tsx_

```tsx
import styled, { CreateStyled } from '@emotion/styled'
import { useTheme, ThemeProvider, EmotionTheming } from '@emotion/core'

type Theme = {
color: {
primary: string
positive: string
negative: string
However, you can define a theme type by extending our type declarations via your own declarations file.

_emotion.d.ts_

```typescript
declare module '@emotion/core' {
export interface Theme {
color: {
primary: string
positive: string
negative: string
}
}
// ...
}

export default styled as CreateStyled<Theme>
// You are also able to use a 3rd party theme this way:
import { MuiTheme } from 'material-ui'

// You can also create themed versions of all the other theme helpers and hooks
const { ThemeProvider, useTheme } = { ThemeProvider, useTheme } as EmotionTheming<Theme>
export { ThemeProvider, useTheme }
declare module '@emotion/core' {
export interface Theme extends MuiTheme {}
}
```

_Button.tsx_

```tsx
import styled from '../path/to/styled'
import styled from '@emotion/styled'

const Button = styled('button')`
padding: 20px;
Expand Down
26 changes: 13 additions & 13 deletions packages/core/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export {
export * from './theming'
export * from './helper'

// tslint:disable-next-line: no-empty-interface
export interface Theme {}

export const ThemeContext: Context<object>
export const CacheProvider: Provider<EmotionCache>
export function withEmotionCache<Props, RefType = any>(
Expand All @@ -57,16 +60,15 @@ export type InterpolationWithTheme<Theme> =
| Interpolation
| ((theme: Theme) => Interpolation)

export interface GlobalProps<Theme> {
styles: InterpolationWithTheme<Theme>
export interface GlobalProps {
styles: Interpolation<Theme>
}

/**
* @desc
* JSX generic are supported only after TS@2.9
*/
export function Global<Theme extends {} = any>(
props: GlobalProps<Theme>
): ReactElement
export function Global(props: GlobalProps): ReactElement

export function keyframes(
template: TemplateStringsArray,
Expand All @@ -83,26 +85,24 @@ export type ClassNamesArg =
| { [className: string]: boolean | null | undefined }
| ArrayClassNamesArg

export interface ClassNamesContent<Theme> {
export interface ClassNamesContent {
css(template: TemplateStringsArray, ...args: Array<Interpolation>): string
css(...args: Array<Interpolation>): string
cx(...args: Array<ClassNamesArg>): string
theme: Theme
}
export interface ClassNamesProps<Theme> {
children(content: ClassNamesContent<Theme>): ReactNode
export interface ClassNamesProps {
children(content: ClassNamesContent): ReactNode
}
/**
* @desc
* JSX generic are supported only after TS@2.9
*/
export function ClassNames<Theme extends {} = any>(
props: ClassNamesProps<Theme>
): ReactElement
export function ClassNames(props: ClassNamesProps): ReactElement

declare module 'react' {
interface DOMAttributes<T> {
css?: InterpolationWithTheme<any>
css?: Interpolation
}
}

Expand All @@ -114,7 +114,7 @@ declare global {
*/

interface IntrinsicAttributes {
css?: InterpolationWithTheme<any>
css?: Interpolation
}
}
}
67 changes: 6 additions & 61 deletions packages/core/types/tests-theming.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,9 @@
// TypeScript Version: 3.1

import * as React from 'react'
import {
useTheme,
ThemeProvider,
withTheme,
EmotionTheming,
WithTheme
} from '@emotion/core'
import styled, { CreateStyled } from '@emotion/styled'
import { useTheme, ThemeProvider, withTheme, Theme } from '@emotion/core'
import { Interpolation, ObjectInterpolation } from '@emotion/styled/base'

interface Theme {
primary: string
secondary: string
}
declare const theme: Theme

interface Props {
Expand Down Expand Up @@ -48,8 +37,6 @@ class CompCWithDefault extends React.Component<Props> {

{
const theme: Theme = useTheme()

const themeFail: Theme = useTheme<number>() // $ExpectError
}

const ThemedFCWithDefault = withTheme(CompFCWithDefault)
Expand All @@ -60,23 +47,6 @@ const ThemedCompWithDefault = withTheme(CompCWithDefault)
;<ThemedCompWithDefault />
;<ThemedCompWithDefault theme={theme} />

const { ThemeProvider: TypedThemeProvider, withTheme: typedWithTheme } = {
ThemeProvider,
withTheme
} as EmotionTheming<Theme>
;<TypedThemeProvider theme={theme} />
// $ExpectError
;<TypedThemeProvider theme={{ primary: 5 }} />

typedWithTheme(CompFC)

/**
* @todo
* Following line should report an error.
*/

typedWithTheme((props: { value: number }) => null)

{
interface Book {
kind: 'book'
Expand All @@ -102,34 +72,12 @@ typedWithTheme((props: { value: number }) => null)
;<Readable kind="book" author="Hejlsberg" />
;<ThemedReadable kind="book" author="Hejlsberg" />
;<Readable kind="magazine" author="Hejlsberg" /> // $ExpectError
;<ThemedReadable kind="magazine" author="Hejlsberg" /> // $ExpectError
}

const themedStyled = styled as CreateStyled<Theme>

const StyledCompC = themedStyled(WrappedCompC)({})
const AdditionallyStyledCompC = themedStyled(StyledCompC)({})
;<StyledCompC prop={true} />
;<AdditionallyStyledCompC prop={true} />

const StyledDiv = themedStyled('div')({})
;<StyledDiv />
// $ExpectError
;<StyledDiv theme={{ primary: 0, secondary: 0 }} />
const AdditionallyStyledDiv = themedStyled(StyledDiv)({})
;<AdditionallyStyledDiv />
// $ExpectError
;<AdditionallyStyledDiv theme={{ primary: 0, secondary: 0 }} />

const StyledDiv2 = themedStyled.div({})
;<StyledDiv2 />
// $ExpectError
;<StyledDiv2 theme={{ primary: 0, secondary: 0 }} />

export type StyleDefinition<T = {}> = Interpolation<WithTheme<T, Theme>>
export type ObjectStyleDefinition<T = {}> = ObjectInterpolation<
WithTheme<T, Theme>
>
type StyleDefinition = Interpolation
type ObjectStyleDefinition = ObjectInterpolation<{
theme: Theme
}>

const style: StyleDefinition = ({ theme }) => ({
color: theme.primary
Expand All @@ -139,7 +87,4 @@ const style2: ObjectStyleDefinition = {
}

// Can use ThemeProvider
;<ThemeProvider theme={{ prop: 'val' }} />
;<TypedThemeProvider theme={{ primary: '', secondary: '' }} />
// $ExpectError
;<TypedThemeProvider theme={{ nope: 'string' }} />
;<ThemeProvider theme={{ primary: 'val' }} />
Andarist marked this conversation as resolved.
Show resolved Hide resolved
23 changes: 11 additions & 12 deletions packages/core/types/tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@
import { ComponentClass } from 'react'
import {
ClassNames,
ClassNamesContent,
Global,
css,
jsx,
keyframes,
withEmotionCache
} from '@emotion/core'
;<Global styles={[]} />

interface TestTheme0 {
resetStyle: any
declare module '@emotion/core' {
// tslint:disable-next-line: strict-export-declare-modifiers
export interface Theme {
primary: string
secondary: string
primaryColor: string
secondaryColor: string
}
}

;<Global styles={(theme: TestTheme0) => [theme.resetStyle]} />
;<Global styles={[]} />
;<Global styles={theme => [theme.primaryColor]} />

declare const getRandomColor: () => string

Expand Down Expand Up @@ -92,14 +97,8 @@ const anim1 = keyframes`
}}
world="of-world"
/>

interface TestTheme1 {
primaryColor: string
secondaryColor: string
}

;<ClassNames>
{({ css, cx, theme }: ClassNamesContent<TestTheme1>) => {
{({ css, cx, theme }) => {
return (
<div>
<span className={cx('a', undefined, 'b', null, [['abc']])} />
Expand Down
29 changes: 14 additions & 15 deletions packages/core/types/theming.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,36 @@
// TypeScript Version: 3.1

import * as React from 'react'
import { Theme } from '@emotion/core'
import { DistributiveOmit, PropsOf } from './helper'

export interface ThemeProviderProps<Theme> {
import {
StyledComponent,
StyledOptions,
CreateStyledComponent,
StyledTags
} from '@emotion/styled'

export interface ThemeProviderProps {
theme: Partial<Theme> | ((outerTheme: Theme) => Theme)
children?: React.ReactNode
}

export interface ThemeProvider<Theme extends {} = any> {
(props: ThemeProviderProps<Theme>): React.ReactElement
export interface ThemeProvider {
(props: ThemeProviderProps): React.ReactElement
}

export type useTheme<Theme extends {} = any> = <T extends Theme = Theme>() => T

export type withTheme<Theme extends {} = any> = <
export type withTheme = <
C extends React.ComponentType<React.ComponentProps<C>>
>(
component: C
) => React.FC<DistributiveOmit<PropsOf<C>, 'theme'> & { theme?: Theme }>

export const ThemeProvider: ThemeProvider
export function useTheme(): Theme

export const useTheme: useTheme
export const ThemeProvider: ThemeProvider

export const withTheme: withTheme

export interface EmotionTheming<Theme> {
ThemeProvider: ThemeProvider<Theme>
useTheme: useTheme<Theme>
withTheme: withTheme<Theme>
}

export type WithTheme<P, T> = P extends { theme: infer Theme }
? P & { theme: Exclude<Theme, undefined> }
: P & { theme: T }
18 changes: 11 additions & 7 deletions packages/serialize/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Definitions by: Junyoung Clare Jang <https://github.com/Ailrun>
// TypeScript Version: 2.8

import { Theme } from '@emotion/core'
import { RegisteredCache, SerializedStyles } from '@emotion/utils'
import * as CSS from 'csstype'

Expand All @@ -15,8 +16,10 @@ export type CSSPropertiesWithMultiValues = {
/**
* @desc Following type exists for autocompletion of key.
*/
export type CSSPseudos<MP> = { [K in CSS.Pseudos]?: ObjectInterpolation<MP> }
export interface CSSOthersObject<MP> {
export type CSSPseudos<MP = { theme: Theme }> = {
[K in CSS.Pseudos]?: ObjectInterpolation<MP>
}
export interface CSSOthersObject<MP = { theme: Theme }> {
[propertiesName: string]: Interpolation<MP>
}

Expand Down Expand Up @@ -56,17 +59,18 @@ export type Keyframes = {
toString: () => string
} & string

export interface ArrayInterpolation<MP> extends Array<Interpolation<MP>> {}
export interface ObjectInterpolation<MP>
export interface ArrayInterpolation<MP = { theme: Theme }>
extends Array<Interpolation<MP>> {}
export interface ObjectInterpolation<MP = { theme: Theme }>
extends CSSPropertiesWithMultiValues,
CSSPseudos<MP>,
CSSOthersObject<MP> {}

export interface FunctionInterpolation<MergedProps> {
export interface FunctionInterpolation<MergedProps = { theme: Theme }> {
(mergedProps: MergedProps): Interpolation<MergedProps>
}

export type Interpolation<MergedProps = undefined> =
export type Interpolation<MergedProps = { theme: Theme }> =
Copy link
Member

Choose a reason for hiding this comment

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

actually I think that Interpolation is also used in css definition and in there you don't have access to Theme, so the previous version with default being undefined was correct for css's case

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this and the serializeStyles stuff should be better viewed by @JakeGinnivan. Not on the same train of thought as he is on with these.

I also invited you @JakeGinnivan as a member to my repo, so it's not mandatory to have to PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

I kinda backed away from these types during my initial work. I wonder if we should just not have a default? (then if not specified it will be unknown)

Copy link
Contributor

Choose a reason for hiding this comment

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

css definition and in there you don't have access to Theme

Just for my understanding, why is this the case? Looking at the other types elsewhere we could actually do another round of simplification if we could add theme into these types directly

Copy link
Member

Choose a reason for hiding this comment

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

css definition and in there you don't have access to Theme

Just for my understanding, why is this the case?

I've meant css function - you can't do css(() => /**/). From what remember such function just gets stringified. css call is being executed without any notion of context, you can hoist it wherever you want etc - so it has no access to theme or props.

A different story is the css props - where you can get access to the builtin Theme.

Looking at the other types elsewhere we could actually do another round of simplification if we could add theme into these types directly

What kind of simplification do you have in mind? I've noticed now that we have Interpolation, CSSInterpolation and even InterpolationWithTheme and I think they might be used sometimes in wrong contexts (especially the Interpolation type). I need to draw out the dependency tree of those types somewhere for myself to analyze this further 😅

| null
| undefined
| boolean
Expand All @@ -79,7 +83,7 @@ export type Interpolation<MergedProps = undefined> =
| ObjectInterpolation<MergedProps>
| FunctionInterpolation<MergedProps>

export function serializeStyles<MP>(
export function serializeStyles<MP = { theme: Theme }>(
Copy link
Member

Choose a reason for hiding this comment

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

same here - serializeStyles gets called by css

args: Array<TemplateStringsArray | Interpolation<MP>>,
registered: RegisteredCache,
mergedProps?: MP
Expand Down
4 changes: 2 additions & 2 deletions packages/serialize/types/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
declare const testTemplateStringsArray: TemplateStringsArray
declare const testKeyframes: Keyframes

const testObjectInterpolation0: ObjectInterpolation<undefined> = {
const testObjectInterpolation0: ObjectInterpolation = {
animation: testKeyframes
}
const testObjectInterpolation1: ObjectInterpolation<undefined> = {
const testObjectInterpolation1: ObjectInterpolation = {
animationName: testKeyframes
}

Expand Down
Loading