Skip to content

Commit 9767309

Browse files
FezVrastaAndarist
authored andcommitted
Fixed styled Flow types (#1570)
* 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
1 parent 539bc0c commit 9767309

File tree

12 files changed

+213
-121
lines changed

12 files changed

+213
-121
lines changed

.changeset/red-chefs-camp.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@emotion/styled-base': patch
3+
'@emotion/styled': patch
4+
---
5+
6+
Fixed package's Flow types

.flowconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@
2323

2424
[options]
2525
suppress_comment=.*\\$FlowFixMe
26+
suppress_comment=.*\\$FlowExpectError
2627
sharedmemory.hash_table_pow=21

.flowconfig-ci

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@
2424

2525
[options]
2626
suppress_comment=.*\\$FlowFixMe
27+
suppress_comment=.*\\$FlowExpectError
2728
server.max_workers=1
2829
sharedmemory.hash_table_pow=21
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/* eslint-disable no-unused-vars */
2+
// @flow
3+
import * as React from 'react'
4+
import createStyled from '../src'
5+
import type { CreateStyledComponent, StyledComponent } from '../src/utils'
6+
7+
export const valid: CreateStyledComponent = createStyled('div')
8+
9+
// $FlowExpectError: we can't cast a StyledComponent to string
10+
export const invalid: string = createStyled('div')
11+
12+
const styled = createStyled('div')
13+
type Props = { color: string }
14+
// prettier-ignore
15+
const Div = styled<Props>({ color: props => props.color })
16+
17+
const validProp = <Div color="red" />
18+
19+
// $FlowExpectError: color property should be a string
20+
const invalidProp = <Div color={2} />
21+
22+
// $FlowExpectError: we don't expose the private StyledComponent properties
23+
const invalidPropAccess = styled().__emotion_base
24+
25+
// We allow styled components not to specify their props types
26+
// NOTE: this is allowed only if you don't attempt to export it!
27+
const untyped: StyledComponent<empty> = styled({})

packages/styled-base/src/index.js

Lines changed: 98 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type { ElementType } from 'react'
44
import {
55
getDefaultShouldForwardProp,
66
type StyledOptions,
7-
type CreateStyled
7+
type CreateStyled,
8+
type PrivateStyledComponent
89
} from './utils'
910
import { withEmotionCache, ThemeContext } from '@emotion/core'
1011
import { getRegisteredStyles, insertStyles } from '@emotion/utils'
@@ -17,12 +18,6 @@ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_liter
1718

1819
let isBrowser = typeof document !== 'undefined'
1920

20-
type StyledComponent = (
21-
props: *
22-
) => React.Node & {
23-
withComponent(nextTag: ElementType, nextOptions?: StyledOptions): *
24-
}
25-
2621
let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
2722
if (process.env.NODE_ENV !== 'production') {
2823
if (tag === undefined) {
@@ -55,7 +50,7 @@ let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
5550
shouldForwardProp || getDefaultShouldForwardProp(baseTag)
5651
const shouldUseAs = !defaultShouldForwardProp('as')
5752

58-
return function(): StyledComponent {
53+
return function<P>(): PrivateStyledComponent<P> {
5954
let args = arguments
6055
let styles =
6156
isReal && tag.__emotion_styles !== undefined
@@ -82,104 +77,107 @@ let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
8277
}
8378
}
8479

85-
const Styled: any = withEmotionCache((props, context, ref) => {
86-
return (
87-
<ThemeContext.Consumer>
88-
{theme => {
89-
const finalTag = (shouldUseAs && props.as) || baseTag
90-
91-
let className = ''
92-
let classInterpolations = []
93-
let mergedProps = props
94-
if (props.theme == null) {
95-
mergedProps = {}
96-
for (let key in props) {
97-
mergedProps[key] = props[key]
80+
// $FlowFixMe: we need to cast StatelessFunctionalComponent to our PrivateStyledComponent class
81+
const Styled: PrivateStyledComponent<P> = withEmotionCache(
82+
(props, context, ref) => {
83+
return (
84+
<ThemeContext.Consumer>
85+
{theme => {
86+
const finalTag = (shouldUseAs && props.as) || baseTag
87+
88+
let className = ''
89+
let classInterpolations = []
90+
let mergedProps = props
91+
if (props.theme == null) {
92+
mergedProps = {}
93+
for (let key in props) {
94+
mergedProps[key] = props[key]
95+
}
96+
mergedProps.theme = theme
97+
}
98+
99+
if (typeof props.className === 'string') {
100+
className = getRegisteredStyles(
101+
context.registered,
102+
classInterpolations,
103+
props.className
104+
)
105+
} else if (props.className != null) {
106+
className = `${props.className} `
98107
}
99-
mergedProps.theme = theme
100-
}
101108

102-
if (typeof props.className === 'string') {
103-
className = getRegisteredStyles(
109+
const serialized = serializeStyles(
110+
styles.concat(classInterpolations),
104111
context.registered,
105-
classInterpolations,
106-
props.className
112+
mergedProps
107113
)
108-
} else if (props.className != null) {
109-
className = `${props.className} `
110-
}
111-
112-
const serialized = serializeStyles(
113-
styles.concat(classInterpolations),
114-
context.registered,
115-
mergedProps
116-
)
117-
const rules = insertStyles(
118-
context,
119-
serialized,
120-
typeof finalTag === 'string'
121-
)
122-
className += `${context.key}-${serialized.name}`
123-
if (targetClassName !== undefined) {
124-
className += ` ${targetClassName}`
125-
}
126-
127-
const finalShouldForwardProp =
128-
shouldUseAs && shouldForwardProp === undefined
129-
? getDefaultShouldForwardProp(finalTag)
130-
: defaultShouldForwardProp
131-
132-
let newProps = {}
133-
134-
for (let key in props) {
135-
if (shouldUseAs && key === 'as') continue
136-
137-
if (
138-
// $FlowFixMe
139-
finalShouldForwardProp(key)
140-
) {
141-
newProps[key] = props[key]
114+
const rules = insertStyles(
115+
context,
116+
serialized,
117+
typeof finalTag === 'string'
118+
)
119+
className += `${context.key}-${serialized.name}`
120+
if (targetClassName !== undefined) {
121+
className += ` ${targetClassName}`
142122
}
143-
}
144123

145-
newProps.className = className
124+
const finalShouldForwardProp =
125+
shouldUseAs && shouldForwardProp === undefined
126+
? getDefaultShouldForwardProp(finalTag)
127+
: defaultShouldForwardProp
146128

147-
newProps.ref = ref || props.innerRef
148-
if (process.env.NODE_ENV !== 'production' && props.innerRef) {
149-
console.error(
150-
'`innerRef` is deprecated and will be removed in a future major version of Emotion, please use the `ref` prop instead' +
151-
(identifierName === undefined
152-
? ''
153-
: ` in the usage of \`${identifierName}\``)
154-
)
155-
}
156-
157-
const ele = React.createElement(finalTag, newProps)
158-
if (!isBrowser && rules !== undefined) {
159-
let serializedNames = serialized.name
160-
let next = serialized.next
161-
while (next !== undefined) {
162-
serializedNames += ' ' + next.name
163-
next = next.next
129+
let newProps = {}
130+
131+
for (let key in props) {
132+
if (shouldUseAs && key === 'as') continue
133+
134+
if (
135+
// $FlowFixMe
136+
finalShouldForwardProp(key)
137+
) {
138+
newProps[key] = props[key]
139+
}
140+
}
141+
142+
newProps.className = className
143+
144+
newProps.ref = ref || props.innerRef
145+
if (process.env.NODE_ENV !== 'production' && props.innerRef) {
146+
console.error(
147+
'`innerRef` is deprecated and will be removed in a future major version of Emotion, please use the `ref` prop instead' +
148+
(identifierName === undefined
149+
? ''
150+
: ` in the usage of \`${identifierName}\``)
151+
)
164152
}
165-
return (
166-
<React.Fragment>
167-
<style
168-
{...{
169-
[`data-emotion-${context.key}`]: serializedNames,
170-
dangerouslySetInnerHTML: { __html: rules },
171-
nonce: context.sheet.nonce
172-
}}
173-
/>
174-
{ele}
175-
</React.Fragment>
176-
)
177-
}
178-
return ele
179-
}}
180-
</ThemeContext.Consumer>
181-
)
182-
})
153+
154+
const ele = React.createElement(finalTag, newProps)
155+
if (!isBrowser && rules !== undefined) {
156+
let serializedNames = serialized.name
157+
let next = serialized.next
158+
while (next !== undefined) {
159+
serializedNames += ' ' + next.name
160+
next = next.next
161+
}
162+
return (
163+
<React.Fragment>
164+
<style
165+
{...{
166+
[`data-emotion-${context.key}`]: serializedNames,
167+
dangerouslySetInnerHTML: { __html: rules },
168+
nonce: context.sheet.nonce
169+
}}
170+
/>
171+
{ele}
172+
</React.Fragment>
173+
)
174+
}
175+
return ele
176+
}}
177+
</ThemeContext.Consumer>
178+
)
179+
}
180+
)
183181

184182
Styled.displayName =
185183
identifierName !== undefined
@@ -204,7 +202,7 @@ let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
204202
) {
205203
return 'NO_COMPONENT_SELECTOR'
206204
}
207-
// $FlowFixMe
205+
// $FlowFixMe: coerce undefined to string
208206
return `.${targetClassName}`
209207
}
210208
})
@@ -220,6 +218,7 @@ let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
220218
: options
221219
)(...styles)
222220
}
221+
223222
return Styled
224223
}
225224
}

packages/styled-base/src/utils.js

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
11
// @flow
22
import * as React from 'react'
3+
import type { ElementType } from 'react'
34
import isPropValid from '@emotion/is-prop-valid'
45

56
export type Interpolations = Array<any>
67

8+
export type StyledOptions = {
9+
label?: string,
10+
shouldForwardProp?: string => boolean,
11+
target?: string
12+
}
13+
14+
export type StyledComponent<P> = React.StatelessFunctionalComponent<P> & {
15+
defaultProps: any,
16+
toString: () => string,
17+
withComponent: (
18+
nextTag: ElementType,
19+
nextOptions?: StyledOptions
20+
) => StyledComponent<P>
21+
}
22+
23+
export type PrivateStyledComponent<P> = StyledComponent<P> & {
24+
__emotion_real: StyledComponent<P>,
25+
__emotion_base: any,
26+
__emotion_styles: any,
27+
__emotion_forwardProp: any
28+
}
29+
730
const testOmitPropsOnStringTag = isPropValid
831
const testOmitPropsOnComponent = (key: string) =>
932
key !== 'theme' && key !== 'innerRef'
@@ -17,19 +40,12 @@ export const getDefaultShouldForwardProp = (tag: React.ElementType) =>
1740
? testOmitPropsOnStringTag
1841
: testOmitPropsOnComponent
1942

20-
export type StyledOptions = {
21-
label?: string,
22-
shouldForwardProp?: string => boolean,
23-
target?: string
24-
}
25-
26-
type CreateStyledComponent = (...args: Interpolations) => *
27-
28-
type BaseCreateStyled = (
29-
tag: React.ElementType,
30-
options?: StyledOptions
31-
) => CreateStyledComponent
43+
export type CreateStyledComponent = <P>(
44+
...args: Interpolations
45+
) => StyledComponent<P>
3246

33-
export type CreateStyled = BaseCreateStyled & {
34-
[key: string]: CreateStyledComponent
47+
export type CreateStyled = {
48+
(tag: React.ElementType, options?: StyledOptions): CreateStyledComponent,
49+
[key: string]: CreateStyledComponent,
50+
bind: () => CreateStyled
3551
}

packages/styled/flow-tests/flow.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// @flow
2+
import * as React from 'react'
3+
import styled from '../src'
4+
5+
type Props = { color: string }
6+
const Foo = styled.div<Props>({
7+
color: 'red'
8+
})
9+
10+
export const valid = <Foo color="red" />
11+
12+
// $FlowExpectError: color must be string
13+
export const invalid = <Foo color={2} />

packages/styled/src/tags.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @flow
12
export const tags = [
23
'a',
34
'abbr',

site/src/components/Box.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,18 @@ const column = props => (props.column ? 'flex-direction:column;' : null)
3636
* ${justify};
3737
* `
3838
*/
39-
const Box = styled.div(
39+
type Props = $Shape<{
40+
className: ?string,
41+
flex: number | string,
42+
children: React$Node,
43+
direction: Array<string>,
44+
css: { [string]: number | string },
45+
display: string,
46+
fontSize: string | number,
47+
justify: string,
48+
align: string
49+
}>
50+
const Box = styled.div<Props>(
4051
display,
4152
space,
4253
width,

0 commit comments

Comments
 (0)