diff --git a/.changeset/strange-pumas-suffer.md b/.changeset/strange-pumas-suffer.md new file mode 100644 index 000000000..e84c3ad0d --- /dev/null +++ b/.changeset/strange-pumas-suffer.md @@ -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. diff --git a/.prettierrc.yaml b/.prettierrc.yaml index c00fa2d41..afa0c8ad2 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -4,3 +4,6 @@ overrides: - files: "docs/*.md" options: printWidth: 60 +- files: "*.js" + options: + parser: flow diff --git a/docs/docs.yaml b/docs/docs.yaml index b25bd6cdc..615c10b4e 100644 --- a/docs/docs.yaml +++ b/docs/docs.yaml @@ -29,6 +29,7 @@ - source-maps - testing - typescript + - flow # This loads the READMEs instead of files in docs/ - title: Packages diff --git a/docs/flow.mdx b/docs/flow.mdx new file mode 100644 index 000000000..60ccc5579 --- /dev/null +++ b/docs/flow.mdx @@ -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('div')` + color: red; +` + +// Option B +const B = styled.div({ + 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('a')` + color: red; +` + +const App = () => Click me +``` + +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 = () => +``` + +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('div')` + color: red; +` + +export const App = () => +``` + + +```jsx +import type { ElementConfig } from 'react' +import styled from '@emotion/styled' + +const Container = styled>('div')` + background-color: yellow; +` + +const App = () => ( + {() => 10} + ^^^^^^^^^^ 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 => +``` + + + diff --git a/package.json b/package.json index a6f0a0fc7..4516471b9 100644 --- a/package.json +++ b/package.json @@ -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, diff --git a/packages/styled-base/flow-tests/flow.js b/packages/styled-base/flow-tests/flow.js index 110dd2b49..12b670f4d 100644 --- a/packages/styled-base/flow-tests/flow.js +++ b/packages/styled-base/flow-tests/flow.js @@ -2,17 +2,23 @@ // @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 = 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({ color: props => props.color }) +const Div = createStyled.div({ color: props => props.color }) const validProp =
@@ -20,8 +26,13 @@ const validProp =
const invalidProp =
// $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 = styled({}) +const untyped: StyledComponent = createStyled.div({}) + +// Style a functional component +const styledFn = createStyled(props =>
)` + color: red; +` diff --git a/packages/styled-base/src/index.js b/packages/styled-base/src/index.js index ca79488e3..568649278 100644 --- a/packages/styled-base/src/index.js +++ b/packages/styled-base/src/index.js @@ -50,7 +50,7 @@ let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => { shouldForwardProp || getDefaultShouldForwardProp(baseTag) const shouldUseAs = !defaultShouldForwardProp('as') - return function

(): PrivateStyledComponent

{ + return function(): PrivateStyledComponent { let args = arguments let styles = isReal && tag.__emotion_styles !== undefined @@ -78,7 +78,7 @@ let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => { } // $FlowFixMe: we need to cast StatelessFunctionalComponent to our PrivateStyledComponent class - const Styled: PrivateStyledComponent

= withEmotionCache( + const Styled: PrivateStyledComponent = withEmotionCache( (props, context, ref) => { return ( diff --git a/packages/styled-base/src/utils.js b/packages/styled-base/src/utils.js index 6e4ac85fd..c06191fd0 100644 --- a/packages/styled-base/src/utils.js +++ b/packages/styled-base/src/utils.js @@ -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 @@ -11,17 +10,17 @@ export type StyledOptions = { target?: string } -export type StyledComponent

= React.StatelessFunctionalComponent

& { +export type StyledComponent = StatelessFunctionalComponent & { defaultProps: any, toString: () => string, withComponent: ( nextTag: ElementType, nextOptions?: StyledOptions - ) => StyledComponent

+ ) => StyledComponent } -export type PrivateStyledComponent

= StyledComponent

& { - __emotion_real: StyledComponent

, +export type PrivateStyledComponent = StyledComponent & { + __emotion_real: StyledComponent, __emotion_base: any, __emotion_styles: any, __emotion_forwardProp: any @@ -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 @@ -40,12 +39,15 @@ export const getDefaultShouldForwardProp = (tag: React.ElementType) => ? testOmitPropsOnStringTag : testOmitPropsOnComponent -export type CreateStyledComponent =

( +export type CreateStyledComponent = ( ...args: Interpolations -) => StyledComponent

+) => StyledComponent export type CreateStyled = { - (tag: React.ElementType, options?: StyledOptions): CreateStyledComponent, + ( + tag: ElementType, + options?: StyledOptions + ): (...args: Interpolations) => StyledComponent, [key: string]: CreateStyledComponent, bind: () => CreateStyled } diff --git a/packages/styled/flow-tests/flow.js b/packages/styled/flow-tests/flow.js index 0754fdc58..c0fdee630 100644 --- a/packages/styled/flow-tests/flow.js +++ b/packages/styled/flow-tests/flow.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars */ // @flow import * as React from 'react' import styled from '../src' @@ -7,7 +8,13 @@ const Foo = styled.div({ color: 'red' }) -export const valid = +const valid = // $FlowExpectError: color must be string -export const invalid = +const invalid = + +// components defined using the root method should be identical +// to the ones generated using the shortcuts +const root: typeof Foo = styled('div')` + colors: red; +` diff --git a/packages/styled/src/index.js b/packages/styled/src/index.js index eea895068..3916bef12 100644 --- a/packages/styled/src/index.js +++ b/packages/styled/src/index.js @@ -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) }) diff --git a/packages/utils/src/types.js b/packages/utils/src/types.js index 903dca8cf..173b5063e 100644 --- a/packages/utils/src/types.js +++ b/packages/utils/src/types.js @@ -1,8 +1,5 @@ // @flow -/*:: -import { StyleSheet } from '@emotion/sheet' - -*/ +import type { StyleSheet } from '@emotion/sheet' export type RegisteredCache = { [string]: string } diff --git a/playgrounds/razzle/src/index.js b/playgrounds/razzle/src/index.js index f4b87635e..5f0c00988 100644 --- a/playgrounds/razzle/src/index.js +++ b/playgrounds/razzle/src/index.js @@ -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 diff --git a/site/src/components/Title.js b/site/src/components/Title.js index 03d5e3fb1..a6197aad5 100644 --- a/site/src/components/Title.js +++ b/site/src/components/Title.js @@ -6,7 +6,7 @@ import * as markdownComponents from '../utils/markdown-styles' type Props = { children: React$Node } -export default styled(markdownComponents.h1)( +export default styled(markdownComponents.h1)( mq({ paddingTop: 0, marginTop: 0, diff --git a/site/src/pages/404.js b/site/src/pages/404.js index 86f092961..84df26030 100644 --- a/site/src/pages/404.js +++ b/site/src/pages/404.js @@ -11,5 +11,4 @@ const NotFoundPage = () => { ) } - export default NotFoundPage