Skip to content

Commit

Permalink
Localized jsx namespace (#1941)
Browse files Browse the repository at this point in the history
* Provide local JSX namespace for `jsx`

* Reuse global JSX namespace defined by React to declare our own

* Add a .d.ts file that can be included in pragma-less projects

* Fixed TSLint errors

* Fix false positive dtslint errors

* Do not allow css prop on props-less components

* add changeset

* Add short docs for css prop + TS
  • Loading branch information
Andarist authored Jul 30, 2020
1 parent 1e10d8c commit f57a722
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 25 deletions.
13 changes: 13 additions & 0 deletions .changeset/moody-ravens-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@emotion/react': patch
---

The way in which we provide TypeScript support for `css` prop has changed. Based on usage of our jsx pragma we are able to add support for `css` prop only for components that support `className` prop (as our `jsx` factory function takes provided `css` prop, resolves it and pass the generated `className` to the rendered component). This has been implemented using technique described [here](https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions). What is important - we no longer extend any global interfaces, so people shouldn't bump anymore into type conflicts for the `css` prop when using different libraries with the `css` prop support, such as `styled-components`.

However, it's not possible to leverage `css` prop support being added conditionally based on a type of rendered component when one is not using our jsx pragma. For those cases when people use our pragma implicitly (for example when using our `@emotion/babel-preset-css-prop`) we have added special file that can be imported once to add support for the `css` prop globally, for all components. Use it like this:

```ts
import {} from '@emotion/react/types/css-prop'
```

In this particular case we are forced to extend the existing `React.Attributes` interface. Previously we've been extending both `React.DOMAttributes<T>` and `JSX.IntrinsicAttributes`. This change is really minor and shouldn't affect any consuming code.
5 changes: 0 additions & 5 deletions .changeset/tidy-ghosts-smile.md

This file was deleted.

10 changes: 10 additions & 0 deletions docs/typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ import { css } from '@emotion'
<div className={css({ background: 'black' })} />
```

### `css` prop

When using our jsx pragma the support for `css` prop is being added only for components that accepts `className` prop, as our `jsx` factory function takes provided `css` prop, resolves it and pass the generated `className` to the rendered component.

However, it's not possible to leverage `css` prop support being added conditionally based on a type of rendered component when one is not using our jsx pragma. For those cases when people use our pragma implicitly (for example when using our `@emotion/babel-preset-css-prop`) we have a special file that can be imported once to add support for the `css` prop globally, for all components. Use it like this:

```ts
import {} from '@emotion/react/types/css-prop'
```

## @emotion/styled

### HTML/SVG elements
Expand Down
9 changes: 9 additions & 0 deletions packages/react/types/css-prop.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {} from 'react'
import { Interpolation } from '@emotion/serialize'
import { Theme } from '.'

declare module 'react' {
interface Attributes {
css?: Interpolation<Theme>
}
}
44 changes: 38 additions & 6 deletions packages/react/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Definitions by: Junyoung Clare Jang <https://github.com/Ailrun>
// TypeScript Version: 3.1
// TypeScript Version: 3.2

import { EmotionCache } from '@emotion/cache'
import {
Expand Down Expand Up @@ -46,8 +46,6 @@ export function withEmotionCache<Props, RefType = any>(
func: (props: Props, context: EmotionCache, ref: Ref<RefType>) => ReactNode
): FC<Props & ClassAttributes<RefType>>

export const jsx: typeof createElement

export function css(
template: TemplateStringsArray,
...args: Array<CSSInterpolation>
Expand Down Expand Up @@ -94,8 +92,42 @@ export interface ClassNamesProps {
*/
export function ClassNames(props: ClassNamesProps): ReactElement

declare module 'react' {
interface Attributes {
css?: Interpolation<Theme>
type WithConditionalCSSProp<P> = 'className' extends keyof P
? (P extends { className?: string } ? P & { css?: Interpolation<Theme> } : P)
: P

// unpack all here to avoid infinite self-referencing when defining our own JSX namespace
type ReactJSXElement = JSX.Element
type ReactJSXElementClass = JSX.ElementClass
type ReactJSXElementAttributesProperty = JSX.ElementAttributesProperty
type ReactJSXElementChildrenAttribute = JSX.ElementChildrenAttribute
type ReactJSXLibraryManagedAttributes<C, P> = JSX.LibraryManagedAttributes<C, P>
type ReactJSXIntrinsicAttributes = JSX.IntrinsicAttributes
type ReactJSXIntrinsicClassAttributes<T> = JSX.IntrinsicClassAttributes<T>
type ReactJSXIntrinsicElements = JSX.IntrinsicElements

export const jsx: typeof createElement
export namespace jsx {
namespace JSX {
interface Element extends ReactJSXElement {}
interface ElementClass extends ReactJSXElementClass {}
interface ElementAttributesProperty
extends ReactJSXElementAttributesProperty {}
interface ElementChildrenAttribute
extends ReactJSXElementChildrenAttribute {}

type LibraryManagedAttributes<C, P> = C extends React.ComponentType<infer T>
? WithConditionalCSSProp<T>
: WithConditionalCSSProp<ReactJSXLibraryManagedAttributes<C, P>>

interface IntrinsicAttributes extends ReactJSXIntrinsicAttributes {}
interface IntrinsicClassAttributes<T>
extends ReactJSXIntrinsicClassAttributes<T> {}

type IntrinsicElements = {
[K in keyof ReactJSXIntrinsicElements]: ReactJSXIntrinsicElements[K] & {
css?: Interpolation<Theme>
}
}
}
}
55 changes: 53 additions & 2 deletions packages/react/types/tests.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @jsx jsx */
import { ComponentClass } from 'react'
import * as React from 'react'
import {
ClassNames,
Global,
Expand Down Expand Up @@ -58,7 +58,10 @@ const ComponentWithCache = withEmotionCache((_props: {}, cache) => {
`}
/>

declare const MyComponent: ComponentClass<{ className?: string; world: string }>
declare const MyComponent: React.ComponentClass<{
className?: string
world: string
}>
;<MyComponent
css={{
backgroundColor: 'black'
Expand Down Expand Up @@ -125,3 +128,51 @@ const anim1 = keyframes`
color: ${theme.secondaryColor};
`}
/>

{
const CompWithClassNameSupport = (_props: {
prop1: string
className?: string
}) => {
return null
}
;<CompWithClassNameSupport
prop1="test"
css={{
backgroundColor: 'hotpink'
}}
/>

const MemoedCompWithClassNameSupport = React.memo(CompWithClassNameSupport)
;<MemoedCompWithClassNameSupport
prop1="test"
css={{
backgroundColor: 'hotpink'
}}
/>
}

{
const CompWithoutClassNameSupport = (_props: { prop1: string }) => {
return null
}

// TS@next reports an error on a different line, so this has to be in a single line so `test:typescript` can validate this on all TS versions correctly
// $ExpectError
;<CompWithoutClassNameSupport prop1="test" css={{ backgroundColor: 'hotpink' }} />

const MemoedCompWithoutClassNameSupport = React.memo(
CompWithoutClassNameSupport
)
// TS@next reports an error on a different line, so this has to be in a single line so `test:typescript` can validate this on all TS versions correctly
// $ExpectError
;<MemoedCompWithoutClassNameSupport prop1="test" css={{ backgroundColor: 'hotpink' }} />
}

{
const CompWithoutProps = (_props: {}) => {
return null
}
// $ExpectError
;<CompWithoutProps css={{ backgroundColor: 'hotpink' }} />
}
15 changes: 4 additions & 11 deletions packages/react/types/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
"baseUrl": "../",
"forceConsistentCasingInFileNames": true,
"jsx": "react",
"lib": [
"es6",
"dom"
],
"lib": ["es6", "dom"],
"module": "commonjs",
"noEmit": true,
"noImplicitAny": true,
Expand All @@ -15,13 +12,9 @@
"strictNullChecks": true,
"strictFunctionTypes": true,
"target": "es5",
"typeRoots": [
"../"
],
"typeRoots": ["../"],
"types": []
},
"include": [
"./*.ts",
"./*.tsx"
]
"include": ["./*.ts", "./*.tsx"],
"exclude": ["./css-prop.d.ts"]
}
4 changes: 3 additions & 1 deletion packages/react/types/tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
],
"no-null-undefined-union": false,
"no-object-literal-type-assertion": false,
"no-unnecessary-generics": false
"no-unnecessary-generics": false,
"no-empty-interface": false,
"strict-export-declare-modifiers": false
}
}

0 comments on commit f57a722

Please sign in to comment.