Skip to content

Commit

Permalink
Make StyledComponent polymorphic (#1588)
Browse files Browse the repository at this point in the history
* chore: tell Prettier we are using Flow

this gets rid of a lot of warnings and wrong auto formatting

* fix: make CreateStyled callable function polymorphic

This makes it possible to type styled functional components and enables a workaround for lack of tagged templates support of Flow

* chore: update type annotation to use new style

* fix: wrong formatting

* style: fix prettier issues

* chore: changeset

* fix: just import the @emotion/sheet type

* style: make prettier happy

* docs: added Flow types documentation page

* Update flow.mdx

* refactor: rename P to Props

# Conflicts:
#	packages/styled-base/src/utils.js
  • Loading branch information
FezVrasta authored and emmatown committed Nov 5, 2019
1 parent a72e6dc commit 2293547
Show file tree
Hide file tree
Showing 14 changed files with 172 additions and 32 deletions.
6 changes: 6 additions & 0 deletions .changeset/strange-pumas-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@emotion/styled-base': patch
'@emotion/styled': patch
---

StyledComponent Flow type is now polymorphic, that means you can now define the component prop types to get better type safety.
3 changes: 3 additions & 0 deletions .prettierrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ overrides:
- files: "docs/*.md"
options:
printWidth: 60
- files: "*.js"
options:
parser: flow
1 change: 1 addition & 0 deletions docs/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- source-maps
- testing
- typescript
- flow

# This loads the READMEs instead of files in docs/
- title: Packages
Expand Down
115 changes: 115 additions & 0 deletions docs/flow.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
title: 'Flow'
---

Emotion is built with Flow, so it exports type definitions for most of its packages,
including `@emotion/styled`.

## @emotion/styled

The styled package can be used to define styled components in two ways, by calling `styled()`,
or by using the `styled.*` shortcuts.

Unfortunately, Flow doesn't currently support generic types on tagged templates, this means if
you'd like to explictly type a styled component props, you will have to use one of the following
alternatives:

```jsx
import styled from '@emotion/styled'

// Option A
const A = styled<Props>('div')`
color: red;
`

// Option B
const B = styled.div<Props>({
color: 'red',
})
```

Styled components are annotated the same way normal React components are:

```jsx
import styled from '@emotion/styled'

type Props = { a: string }
const Link = styled<Props>('a')`
color: red;
`

const App = () => <Link href="#">Click me</Link>
```

Just like for normal React components, you don't need to provide type annotations
for your styled components if you don't plan to export them from your module:

```jsx
import styled from '@emotion/styled'

const Internal = styled.div`
color: red;
`
```

Be aware, Flow infers the return type of your components by referencing their return type,
this means you will need to annotate the properties of the root component in the case below:

```jsx

const Container = styled.div`
^^^^^^^^^^^ Missing type annotation for P. P is a type parameter declared in function type [1] and was implicitly instantiated at
encaps tag [2].
color: red;
`

export const App = () => <Container />
```

You can use `React$ElementConfig` to obtain the props type of a HTML tag, or of
any existing React component:

```jsx
import type { ElementConfig } from 'react'

type Props = ElementConfig<'div'>
const Container = styled<Props>('div')`
color: red;
`

export const App = () => <Container />
```


```jsx
import type { ElementConfig } from 'react'
import styled from '@emotion/styled'

const Container = styled<ElementConfig<'div'>>('div')`
background-color: yellow;
`

const App = () => (
<Container>{() => 10}</Container>
^^^^^^^^^^ Cannot create Container element because in property children:
• Either inexact function [1] is incompatible with exact React.Element [2].
• Or function [1] is incompatible with React.Portal [3].
• Or property @@iterator is missing in function [1] but exists in $Iterable [4].
)
```

Alternatively, you can define the return type of your component, so that
Flow doesn't need to infer it reading the props type of the internal component:

```jsx
import type { Node } from 'react'

const Container = styled.div`
color: red;
`

export const App = (): Node => <Container />
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
],
"parser": "babel-eslint",
"rules": {
"prettier/prettier": "error",
"prettier/prettier": ["error", {"parser": "flow"}],
"react/prop-types": 0,
"react/no-unused-prop-types": 0,
"standard/computed-property-even-spacing": 0,
Expand Down
27 changes: 19 additions & 8 deletions packages/styled-base/flow-tests/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,37 @@
// @flow
import * as React from 'react'
import createStyled from '../src'
import type { CreateStyledComponent, StyledComponent } from '../src/utils'
import type {
CreateStyledComponent,
StyledComponent,
Interpolations
} from '../src/utils'

export const valid: CreateStyledComponent = createStyled('div')
type Props = { color: string }

// It returns the expected type
export const valid: (
...args: Interpolations
) => StyledComponent<Props> = createStyled('div')

// $FlowExpectError: we can't cast a StyledComponent to string
export const invalid: string = createStyled('div')

const styled = createStyled('div')
type Props = { color: string }
// prettier-ignore
const Div = styled<Props>({ color: props => props.color })
const Div = createStyled.div<Props>({ color: props => props.color })

const validProp = <Div color="red" />

// $FlowExpectError: color property should be a string
const invalidProp = <Div color={2} />

// $FlowExpectError: we don't expose the private StyledComponent properties
const invalidPropAccess = styled().__emotion_base
const invalidPropAccess = createStyled().__emotion_base

// We allow styled components not to specify their props types
// NOTE: this is allowed only if you don't attempt to export it!
const untyped: StyledComponent<empty> = styled({})
const untyped: StyledComponent<empty> = createStyled.div({})

// Style a functional component
const styledFn = createStyled<Props>(props => <div {...props} />)`
color: red;
`
4 changes: 2 additions & 2 deletions packages/styled-base/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
shouldForwardProp || getDefaultShouldForwardProp(baseTag)
const shouldUseAs = !defaultShouldForwardProp('as')

return function<P>(): PrivateStyledComponent<P> {
return function<Props>(): PrivateStyledComponent<Props> {
let args = arguments
let styles =
isReal && tag.__emotion_styles !== undefined
Expand Down Expand Up @@ -78,7 +78,7 @@ let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
}

// $FlowFixMe: we need to cast StatelessFunctionalComponent to our PrivateStyledComponent class
const Styled: PrivateStyledComponent<P> = withEmotionCache(
const Styled: PrivateStyledComponent<Props> = withEmotionCache(
(props, context, ref) => {
return (
<ThemeContext.Consumer>
Expand Down
22 changes: 12 additions & 10 deletions packages/styled-base/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// @flow
import * as React from 'react'
import type { ElementType } from 'react'
import type { ElementType, StatelessFunctionalComponent } from 'react'
import isPropValid from '@emotion/is-prop-valid'

export type Interpolations = Array<any>
Expand All @@ -11,17 +10,17 @@ export type StyledOptions = {
target?: string
}

export type StyledComponent<P> = React.StatelessFunctionalComponent<P> & {
export type StyledComponent<Props> = StatelessFunctionalComponent<Props> & {
defaultProps: any,
toString: () => string,
withComponent: (
nextTag: ElementType,
nextOptions?: StyledOptions
) => StyledComponent<P>
) => StyledComponent<Props>
}

export type PrivateStyledComponent<P> = StyledComponent<P> & {
__emotion_real: StyledComponent<P>,
export type PrivateStyledComponent<Props> = StyledComponent<Props> & {
__emotion_real: StyledComponent<Props>,
__emotion_base: any,
__emotion_styles: any,
__emotion_forwardProp: any
Expand All @@ -31,7 +30,7 @@ const testOmitPropsOnStringTag = isPropValid
const testOmitPropsOnComponent = (key: string) =>
key !== 'theme' && key !== 'innerRef'

export const getDefaultShouldForwardProp = (tag: React.ElementType) =>
export const getDefaultShouldForwardProp = (tag: ElementType) =>
typeof tag === 'string' &&
// 96 is one less than the char code
// for "a" so this is checking that
Expand All @@ -40,12 +39,15 @@ export const getDefaultShouldForwardProp = (tag: React.ElementType) =>
? testOmitPropsOnStringTag
: testOmitPropsOnComponent

export type CreateStyledComponent = <P>(
export type CreateStyledComponent = <Props>(
...args: Interpolations
) => StyledComponent<P>
) => StyledComponent<Props>

export type CreateStyled = {
(tag: React.ElementType, options?: StyledOptions): CreateStyledComponent,
<Props>(
tag: ElementType,
options?: StyledOptions
): (...args: Interpolations) => StyledComponent<Props>,
[key: string]: CreateStyledComponent,
bind: () => CreateStyled
}
11 changes: 9 additions & 2 deletions packages/styled/flow-tests/flow.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
// @flow
import * as React from 'react'
import styled from '../src'
Expand All @@ -7,7 +8,13 @@ const Foo = styled.div<Props>({
color: 'red'
})

export const valid = <Foo color="red" />
const valid = <Foo color="red" />

// $FlowExpectError: color must be string
export const invalid = <Foo color={2} />
const invalid = <Foo color={2} />

// components defined using the root method should be identical
// to the ones generated using the shortcuts
const root: typeof Foo = styled<Props>('div')`
colors: red;
`
1 change: 1 addition & 0 deletions packages/styled/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { tags } from './tags'
const newStyled = styled.bind()

tags.forEach(tagName => {
// $FlowFixMe: we can ignore this because its exposed type is defined by the CreateStyled type
newStyled[tagName] = newStyled(tagName)
})

Expand Down
5 changes: 1 addition & 4 deletions packages/utils/src/types.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
// @flow
/*::
import { StyleSheet } from '@emotion/sheet'
*/
import type { StyleSheet } from '@emotion/sheet'

export type RegisteredCache = { [string]: string }

Expand Down
4 changes: 1 addition & 3 deletions playgrounds/razzle/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ server.listen(process.env.PORT || 3000, error => {
}

console.log('🚀 started')
})

// $FlowFixMe
}) // $FlowFixMe
if (module.hot) {
console.log('✅ Server-side HMR Enabled!')
// $FlowFixMe
Expand Down
2 changes: 1 addition & 1 deletion site/src/components/Title.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as markdownComponents from '../utils/markdown-styles'
type Props = {
children: React$Node
}
export default styled(markdownComponents.h1)<Props>(
export default styled<Props>(markdownComponents.h1)(
mq({
paddingTop: 0,
marginTop: 0,
Expand Down
1 change: 0 additions & 1 deletion site/src/pages/404.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,4 @@ const NotFoundPage = () => {
</Layout>
)
}

export default NotFoundPage

0 comments on commit 2293547

Please sign in to comment.