Skip to content

Commit

Permalink
Create new overload for CreateStyled which supports shouldForwardProp…
Browse files Browse the repository at this point in the history
… being a custom type guard (#1624)

* Created overload of create styled which supports shouldForwardProp being a custom type guard

* Add test case for filtering conflicting props

* Add docs and overload for normal components

* Update .changeset/polite-tips-attend/changes.md

Co-Authored-By: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
JakeGinnivan and Andarist committed Nov 14, 2019
1 parent 99c6b7e commit ad77ed2
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 5 deletions.
4 changes: 4 additions & 0 deletions .changeset/polite-tips-attend/changes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"releases": [{ "name": "@emotion/styled", "type": "minor" }],
"dependents": []
}
15 changes: 15 additions & 0 deletions .changeset/polite-tips-attend/changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Added `CreateStyled` overload to handle when `shouldForwardProp` is a custom type guard for intrinsic props.

As an example, if you want to override the type of the `color` prop:

```ts
export const Box = styled('div', {
shouldForwardProp: (
propName
): propName is Exclude<keyof JSX.IntrinsicElements['div'], 'color'> =>
propName !== 'color'
})<{ color: Array<string> }>(props => ({
color: props.color[0]
}))
;<Box color={['green']} />
```
27 changes: 27 additions & 0 deletions docs/typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,33 @@ const App = () => (
)
```

### Forwarding props

Sometimes you want to wrap an existing component and override the type of a prop. Emotion allows you to specify a `shouldForwardProp` hook to filter properties which should be passed to the wrapped component.

If you make should `shouldForwardProp` a [type guard](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) then only the props from the type guard will be exposed.

For example:

``` ts
const Original: React.FC<{ prop1: string, prop2: string }> = () => null

interface StyledOriginalExtraProps {
// This prop would conflict with the `prop2` on Original
prop2: number
}
const StyledOriginal = styled(Original, {
// Filter prop2 by only forwarding prop1
shouldForwardProp: (propName): propName is 'prop1' => propName === 'prop1'
})<StyledOriginalExtraProps>(props => {
// props.prop2 will be `number`
return {}
})

// No more type conflict error
;<StyledOriginal prop1="1" prop2={2} />
```

### Passing props when styling a React component

```tsx
Expand Down
44 changes: 40 additions & 4 deletions packages/styled/types/base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,19 @@ export {

export { ComponentSelector, Interpolation, PropsOf, DistributiveOmit }

export interface StyledOptions {
/** Same as StyledOptions but shouldForwardProp must be a type guard */
export interface FilteringStyledOptions<
Props,
ForwardedProps extends keyof Props = keyof Props
> {
label?: string
shouldForwardProp?(propName: keyof Props): propName is ForwardedProps
target?: string
}

export interface StyledOptions<Props> {
label?: string
shouldForwardProp?(propName: string): boolean
shouldForwardProp?(propName: keyof Props): boolean
target?: string
}

Expand Down Expand Up @@ -90,14 +100,40 @@ export interface CreateStyledComponent<
* @example styled('div')<Props>(props => ({ width: props.width })
*/
export interface CreateStyled<Theme extends {} = any> {
<
C extends React.ComponentType<React.ComponentProps<C>>,
ForwardedProps extends keyof React.ComponentProps<
C
> = keyof React.ComponentProps<C>
>(
component: C,
options: FilteringStyledOptions<PropsOf<C>, ForwardedProps>
): CreateStyledComponent<
Pick<PropsOf<C>, ForwardedProps> & { theme?: Theme },
{},
{ theme: Theme }
>

<C extends React.ComponentType<React.ComponentProps<C>>>(
component: C,
options?: StyledOptions
options?: StyledOptions<PropsOf<C>>
): CreateStyledComponent<PropsOf<C> & { theme?: Theme }, {}, { theme: Theme }>

<
Tag extends keyof JSX.IntrinsicElements,
ForwardedProps extends keyof JSX.IntrinsicElements[Tag] = keyof JSX.IntrinsicElements[Tag]
>(
tag: Tag,
options: FilteringStyledOptions<JSX.IntrinsicElements[Tag], ForwardedProps>
): CreateStyledComponent<
{ theme?: Theme },
Pick<JSX.IntrinsicElements[Tag], ForwardedProps>,
{ theme: Theme }
>

<Tag extends keyof JSX.IntrinsicElements>(
tag: Tag,
options?: StyledOptions
options?: StyledOptions<JSX.IntrinsicElements[Tag]>
): CreateStyledComponent<
{ theme?: Theme },
JSX.IntrinsicElements[Tag],
Expand Down
4 changes: 3 additions & 1 deletion packages/styled/types/tests-base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const Input3 = styled(View.withComponent('input'))({
/>

const Canvas0 = styled('canvas', {
shouldForwardProp(propName) {
shouldForwardProp(propName): propName is 'width' | 'height' {
return propName === 'width' || propName === 'height'
}
})`
Expand All @@ -94,6 +94,8 @@ const Canvas1 = styled('canvas', {
width: '200px'
})
;<Canvas0 />
// $ExpectError
;<Canvas0 id="id-should-be-filtered" />
;<Canvas1 />

interface PrimaryProps {
Expand Down
31 changes: 31 additions & 0 deletions packages/styled/types/tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,34 @@ const Container2 = styled.div<{ test: number }>(props => ({
const Container3 = styled.div(({ theme }) => ({
width: theme.width
}))

// This example shows using shouldForwardProps to define a prop
// who's type clashes with an intrinsic prop.
// It makes use of a custom type guard on shouldForwardProp to exclude color

export const Box = styled('div', {
shouldForwardProp: (
propName
): propName is Exclude<keyof JSX.IntrinsicElements['div'], 'color'> =>
propName !== 'color'
})<{ color: Array<string> }>(props => ({
color: props.color[0]
}))
;<Box color={['green']} />

const Original: React.FC<{ prop1: string; prop2: string }> = () => null

interface StyledOriginalExtraProps {
// This prop would conflict with the `prop2` on Original
prop2: number
}
const StyledOriginal = styled(Original, {
// Filter prop2 by only forwarding prop1
shouldForwardProp: (propName): propName is 'prop1' => propName === 'prop1'
})<StyledOriginalExtraProps>(props => {
// props.prop2 will be `number`
return {}
})

// No more type conflict error
;<StyledOriginal prop1="1" prop2={2} />

0 comments on commit ad77ed2

Please sign in to comment.