Skip to content

Commit

Permalink
Fixed styled Flow types (emotion-js#1570)
Browse files Browse the repository at this point in the history
* fix: refactor styled-base Flow types to work again

* fix: improve Styled type to accept optional props type

* fix: make styled package return proper types

* fix: missing flow header

* test: test untyped styled component

* chore: fix site types

* docs: comment typo
  • Loading branch information
FezVrasta authored and Andarist committed Oct 27, 2019
1 parent e0c17fa commit 849231d
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 121 deletions.
6 changes: 6 additions & 0 deletions .changeset/red-chefs-camp.md
@@ -0,0 +1,6 @@
---
'@emotion/styled-base': patch
'@emotion/styled': patch
---

Fixed package's Flow types
1 change: 1 addition & 0 deletions .flowconfig
Expand Up @@ -23,4 +23,5 @@

[options]
suppress_comment=.*\\$FlowFixMe
suppress_comment=.*\\$FlowExpectError
sharedmemory.hash_table_pow=21
1 change: 1 addition & 0 deletions .flowconfig-ci
Expand Up @@ -24,5 +24,6 @@

[options]
suppress_comment=.*\\$FlowFixMe
suppress_comment=.*\\$FlowExpectError
server.max_workers=1
sharedmemory.hash_table_pow=21
27 changes: 27 additions & 0 deletions packages/styled-base/flow-tests/flow.js
@@ -0,0 +1,27 @@
/* eslint-disable no-unused-vars */
// @flow
import * as React from 'react'
import createStyled from '../src'
import type { CreateStyledComponent, StyledComponent } from '../src/utils'

export const valid: CreateStyledComponent = 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 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

// 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({})
197 changes: 98 additions & 99 deletions packages/styled-base/src/index.js
Expand Up @@ -4,7 +4,8 @@ import type { ElementType } from 'react'
import {
getDefaultShouldForwardProp,
type StyledOptions,
type CreateStyled
type CreateStyled,
type PrivateStyledComponent
} from './utils'
import { withEmotionCache, ThemeContext } from '@emotion/core'
import { getRegisteredStyles, insertStyles } from '@emotion/utils'
Expand All @@ -17,12 +18,6 @@ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_liter

let isBrowser = typeof document !== 'undefined'

type StyledComponent = (
props: *
) => React.Node & {
withComponent(nextTag: ElementType, nextOptions?: StyledOptions): *
}

let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
if (process.env.NODE_ENV !== 'production') {
if (tag === undefined) {
Expand Down Expand Up @@ -55,7 +50,7 @@ let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
shouldForwardProp || getDefaultShouldForwardProp(baseTag)
const shouldUseAs = !defaultShouldForwardProp('as')

return function(): StyledComponent {
return function<P>(): PrivateStyledComponent<P> {
let args = arguments
let styles =
isReal && tag.__emotion_styles !== undefined
Expand All @@ -82,104 +77,107 @@ let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
}
}

const Styled: any = withEmotionCache((props, context, ref) => {
return (
<ThemeContext.Consumer>
{theme => {
const finalTag = (shouldUseAs && props.as) || baseTag

let className = ''
let classInterpolations = []
let mergedProps = props
if (props.theme == null) {
mergedProps = {}
for (let key in props) {
mergedProps[key] = props[key]
// $FlowFixMe: we need to cast StatelessFunctionalComponent to our PrivateStyledComponent class
const Styled: PrivateStyledComponent<P> = withEmotionCache(
(props, context, ref) => {
return (
<ThemeContext.Consumer>
{theme => {
const finalTag = (shouldUseAs && props.as) || baseTag

let className = ''
let classInterpolations = []
let mergedProps = props
if (props.theme == null) {
mergedProps = {}
for (let key in props) {
mergedProps[key] = props[key]
}
mergedProps.theme = theme
}

if (typeof props.className === 'string') {
className = getRegisteredStyles(
context.registered,
classInterpolations,
props.className
)
} else if (props.className != null) {
className = `${props.className} `
}
mergedProps.theme = theme
}

if (typeof props.className === 'string') {
className = getRegisteredStyles(
const serialized = serializeStyles(
styles.concat(classInterpolations),
context.registered,
classInterpolations,
props.className
mergedProps
)
} else if (props.className != null) {
className = `${props.className} `
}

const serialized = serializeStyles(
styles.concat(classInterpolations),
context.registered,
mergedProps
)
const rules = insertStyles(
context,
serialized,
typeof finalTag === 'string'
)
className += `${context.key}-${serialized.name}`
if (targetClassName !== undefined) {
className += ` ${targetClassName}`
}

const finalShouldForwardProp =
shouldUseAs && shouldForwardProp === undefined
? getDefaultShouldForwardProp(finalTag)
: defaultShouldForwardProp

let newProps = {}

for (let key in props) {
if (shouldUseAs && key === 'as') continue

if (
// $FlowFixMe
finalShouldForwardProp(key)
) {
newProps[key] = props[key]
const rules = insertStyles(
context,
serialized,
typeof finalTag === 'string'
)
className += `${context.key}-${serialized.name}`
if (targetClassName !== undefined) {
className += ` ${targetClassName}`
}
}

newProps.className = className
const finalShouldForwardProp =
shouldUseAs && shouldForwardProp === undefined
? getDefaultShouldForwardProp(finalTag)
: defaultShouldForwardProp

newProps.ref = ref || props.innerRef
if (process.env.NODE_ENV !== 'production' && props.innerRef) {
console.error(
'`innerRef` is deprecated and will be removed in a future major version of Emotion, please use the `ref` prop instead' +
(identifierName === undefined
? ''
: ` in the usage of \`${identifierName}\``)
)
}

const ele = React.createElement(finalTag, newProps)
if (!isBrowser && rules !== undefined) {
let serializedNames = serialized.name
let next = serialized.next
while (next !== undefined) {
serializedNames += ' ' + next.name
next = next.next
let newProps = {}

for (let key in props) {
if (shouldUseAs && key === 'as') continue

if (
// $FlowFixMe
finalShouldForwardProp(key)
) {
newProps[key] = props[key]
}
}

newProps.className = className

newProps.ref = ref || props.innerRef
if (process.env.NODE_ENV !== 'production' && props.innerRef) {
console.error(
'`innerRef` is deprecated and will be removed in a future major version of Emotion, please use the `ref` prop instead' +
(identifierName === undefined
? ''
: ` in the usage of \`${identifierName}\``)
)
}
return (
<React.Fragment>
<style
{...{
[`data-emotion-${context.key}`]: serializedNames,
dangerouslySetInnerHTML: { __html: rules },
nonce: context.sheet.nonce
}}
/>
{ele}
</React.Fragment>
)
}
return ele
}}
</ThemeContext.Consumer>
)
})

const ele = React.createElement(finalTag, newProps)
if (!isBrowser && rules !== undefined) {
let serializedNames = serialized.name
let next = serialized.next
while (next !== undefined) {
serializedNames += ' ' + next.name
next = next.next
}
return (
<React.Fragment>
<style
{...{
[`data-emotion-${context.key}`]: serializedNames,
dangerouslySetInnerHTML: { __html: rules },
nonce: context.sheet.nonce
}}
/>
{ele}
</React.Fragment>
)
}
return ele
}}
</ThemeContext.Consumer>
)
}
)

Styled.displayName =
identifierName !== undefined
Expand All @@ -204,7 +202,7 @@ let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
) {
return 'NO_COMPONENT_SELECTOR'
}
// $FlowFixMe
// $FlowFixMe: coerce undefined to string
return `.${targetClassName}`
}
})
Expand All @@ -220,6 +218,7 @@ let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
: options
)(...styles)
}

return Styled
}
}
Expand Down
44 changes: 30 additions & 14 deletions packages/styled-base/src/utils.js
@@ -1,9 +1,32 @@
// @flow
import * as React from 'react'
import type { ElementType } from 'react'
import isPropValid from '@emotion/is-prop-valid'

export type Interpolations = Array<any>

export type StyledOptions = {
label?: string,
shouldForwardProp?: string => boolean,
target?: string
}

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

export type PrivateStyledComponent<P> = StyledComponent<P> & {
__emotion_real: StyledComponent<P>,
__emotion_base: any,
__emotion_styles: any,
__emotion_forwardProp: any
}

const testOmitPropsOnStringTag = isPropValid
const testOmitPropsOnComponent = (key: string) =>
key !== 'theme' && key !== 'innerRef'
Expand All @@ -17,19 +40,12 @@ export const getDefaultShouldForwardProp = (tag: React.ElementType) =>
? testOmitPropsOnStringTag
: testOmitPropsOnComponent

export type StyledOptions = {
label?: string,
shouldForwardProp?: string => boolean,
target?: string
}

type CreateStyledComponent = (...args: Interpolations) => *

type BaseCreateStyled = (
tag: React.ElementType,
options?: StyledOptions
) => CreateStyledComponent
export type CreateStyledComponent = <P>(
...args: Interpolations
) => StyledComponent<P>

export type CreateStyled = BaseCreateStyled & {
[key: string]: CreateStyledComponent
export type CreateStyled = {
(tag: React.ElementType, options?: StyledOptions): CreateStyledComponent,
[key: string]: CreateStyledComponent,
bind: () => CreateStyled
}
13 changes: 13 additions & 0 deletions packages/styled/flow-tests/flow.js
@@ -0,0 +1,13 @@
// @flow
import * as React from 'react'
import styled from '../src'

type Props = { color: string }
const Foo = styled.div<Props>({
color: 'red'
})

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

// $FlowExpectError: color must be string
export const invalid = <Foo color={2} />
1 change: 1 addition & 0 deletions packages/styled/src/tags.js
@@ -1,3 +1,4 @@
// @flow
export const tags = [
'a',
'abbr',
Expand Down
13 changes: 12 additions & 1 deletion site/src/components/Box.js
Expand Up @@ -36,7 +36,18 @@ const column = props => (props.column ? 'flex-direction:column;' : null)
* ${justify};
* `
*/
const Box = styled.div(
type Props = $Shape<{
className: ?string,
flex: number | string,
children: React$Node,
direction: Array<string>,
css: { [string]: number | string },
display: string,
fontSize: string | number,
justify: string,
align: string
}>
const Box = styled.div<Props>(
display,
space,
width,
Expand Down

0 comments on commit 849231d

Please sign in to comment.