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

Make StyledComponent polymorphic #1588

Merged
merged 11 commits into from
Nov 5, 2019
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;
`
12 changes: 7 additions & 5 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,7 +10,7 @@ export type StyledOptions = {
target?: string
}

export type StyledComponent<P> = React.StatelessFunctionalComponent<P> & {
export type StyledComponent<P> = StatelessFunctionalComponent<P> & {
FezVrasta marked this conversation as resolved.
Show resolved Hide resolved
defaultProps: any,
toString: () => string,
withComponent: (
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 @@ -45,7 +44,10 @@ export type CreateStyledComponent = <P>(
) => StyledComponent<P>

export type CreateStyled = {
(tag: React.ElementType, options?: StyledOptions): CreateStyledComponent,
<P>(
tag: ElementType,
options?: StyledOptions
): (...args: Interpolations) => StyledComponent<P>,
[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