diff --git a/.changeset/tall-weeks-compete.md b/.changeset/tall-weeks-compete.md new file mode 100644 index 000000000..f1f5e1a4a --- /dev/null +++ b/.changeset/tall-weeks-compete.md @@ -0,0 +1,5 @@ +--- +'@emotion/native': minor +--- + +Added TypeScript type definitions. diff --git a/packages/native/package.json b/packages/native/package.json index 6c4a5c76f..22454ffd0 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -5,15 +5,18 @@ "main": "dist/native.cjs.js", "module": "dist/native.esm.js", "scripts": { - "test:typescript": "exit 0" + "test:typescript": "dtslint types" }, "files": [ "src", "dist", "macro.js" ], + "types": "types/index.d.ts", "devDependencies": { "@babel/core": "^7.7.2", + "@types/react-native": "^0.57.0", + "dtslint": "^0.3.0", "react": "^16.11.0", "react-native": "^0.57.0" }, diff --git a/packages/native/src/styled.js b/packages/native/src/base.js similarity index 100% rename from packages/native/src/styled.js rename to packages/native/src/base.js diff --git a/packages/native/src/index.js b/packages/native/src/index.js index ee68fb942..96efa7daa 100644 --- a/packages/native/src/index.js +++ b/packages/native/src/index.js @@ -1,59 +1,50 @@ import * as reactNative from 'react-native' import { createCss } from '@emotion/primitives-core' -import { styled } from './styled' +import { styled } from './base' const css = createCss(reactNative.StyleSheet) const components = [ 'ActivityIndicator', 'ActivityIndicatorIOS', - 'ART', 'Button', 'DatePickerIOS', 'DrawerLayoutAndroid', + 'FlatList', 'Image', 'ImageBackground', - 'ImageEditor', - 'ImageStore', 'KeyboardAvoidingView', 'ListView', - 'MapView', 'Modal', 'NavigatorIOS', 'Picker', 'PickerIOS', 'ProgressBarAndroid', 'ProgressViewIOS', + 'RecyclerViewBackedScrollView', + 'RefreshControl', + 'SafeAreaView', 'ScrollView', + 'SectionList', 'SegmentedControlIOS', 'Slider', - 'SliderIOS', 'SnapshotViewIOS', - 'Switch', - 'RecyclerViewBackedScrollView', - 'RefreshControl', - 'SafeAreaView', 'StatusBar', 'SwipeableListView', - 'SwitchAndroid', + 'Switch', 'SwitchIOS', 'TabBarIOS', 'Text', 'TextInput', - 'ToastAndroid', 'ToolbarAndroid', - 'Touchable', 'TouchableHighlight', 'TouchableNativeFeedback', 'TouchableOpacity', 'TouchableWithoutFeedback', 'View', 'ViewPagerAndroid', - 'WebView', - 'FlatList', - 'SectionList', - 'VirtualizedList' + 'WebView' ] export { css } diff --git a/packages/native/types/base.d.ts b/packages/native/types/base.d.ts new file mode 100644 index 000000000..01f49e87c --- /dev/null +++ b/packages/native/types/base.d.ts @@ -0,0 +1,244 @@ +// Definitions by: Pat Sissons +// TypeScript Version: 3.4 + +import { + ComponentPropsWithoutRef, + ComponentType, + NamedExoticComponent, + PropsWithChildren +} from 'react' +import { Theme } from '@emotion/core' +import * as RN from 'react-native' + +type ReactNative = typeof RN + +export type ReactNativeStyle = RN.ViewStyle | RN.TextStyle | RN.ImageStyle + +export type ReactNativeComponentNames = + | 'ActivityIndicator' + | 'ActivityIndicatorIOS' + | 'Button' + | 'DatePickerIOS' + | 'DrawerLayoutAndroid' + | 'FlatList' + | 'Image' + | 'ImageBackground' + | 'KeyboardAvoidingView' + | 'ListView' + | 'Modal' + | 'NavigatorIOS' + | 'Picker' + | 'PickerIOS' + | 'ProgressBarAndroid' + | 'ProgressViewIOS' + | 'RecyclerViewBackedScrollView' + | 'RefreshControl' + | 'SafeAreaView' + | 'ScrollView' + | 'SectionList' + | 'SegmentedControlIOS' + | 'Slider' + | 'SnapshotViewIOS' + | 'StatusBar' + | 'SwipeableListView' + | 'Switch' + | 'SwitchIOS' + | 'TabBarIOS' + | 'Text' + | 'TextInput' + | 'ToolbarAndroid' + | 'TouchableHighlight' + | 'TouchableNativeFeedback' + | 'TouchableOpacity' + | 'TouchableWithoutFeedback' + | 'View' + | 'ViewPagerAndroid' + | 'WebView' + +export type ReactNativeComponents = Pick + +export type ReactNativeComponentProps< + ComponentName extends ReactNativeComponentNames +> = ComponentPropsWithoutRef + +export type ReactNativeStyleType = Props extends { + style?: RN.StyleProp +} + ? StyleType extends ReactNativeStyle ? StyleType : ReactNativeStyle + : ReactNativeStyle + +export type InterpolationPrimitive< + StyleType extends ReactNativeStyle = ReactNativeStyle +> = + | null + | undefined + | boolean + | number + | string + | ObjectInterpolation + +export type ObjectInterpolation< + StyleType extends ReactNativeStyle = ReactNativeStyle +> = StyleType + +export interface ArrayCSSInterpolation< + StyleType extends ReactNativeStyle = ReactNativeStyle +> extends Array> {} + +export type CSSInterpolation< + StyleType extends ReactNativeStyle = ReactNativeStyle +> = InterpolationPrimitive | ArrayCSSInterpolation + +export interface ArrayInterpolation< + MergedProps, + StyleType extends ReactNativeStyle = ReactNativeStyle +> extends Array> {} + +export interface FunctionInterpolation< + MergedProps, + StyleType extends ReactNativeStyle = ReactNativeStyle +> { + (mergedProps: MergedProps): Interpolation +} + +export type Interpolation< + MergedProps = unknown, + StyleType extends ReactNativeStyle = ReactNativeStyle +> = + | InterpolationPrimitive + | ArrayInterpolation + | FunctionInterpolation + +/** Same as StyledOptions but shouldForwardProp must be a type guard */ +export interface FilteringStyledOptions< + Props, + ForwardedProps extends keyof Props = keyof Props +> { + shouldForwardProp?(propName: PropertyKey): propName is ForwardedProps +} + +export interface StyledOptions { + shouldForwardProp?(propName: PropertyKey): boolean +} + +/** + * @typeparam ComponentProps Props which will be included when withComponent is called + * @typeparam SpecificComponentProps Props which will *not* be included when withComponent is called + */ +export interface StyledComponent< + ComponentProps extends {}, + SpecificComponentProps extends {} = {} +> + extends NamedExoticComponent< + PropsWithChildren + > { + withComponent< + Component extends ComponentType> + >( + component: Component + ): StyledComponent> + withComponent( + component: ReactNativeComponents[ComponentName] + ): StyledComponent> +} + +/** + * @typeparam ComponentProps Props which will be included when withComponent is called + * @typeparam SpecificComponentProps Props which will *not* be included when withComponent is called + */ +export interface CreateStyledComponent< + ComponentProps extends {}, + SpecificComponentProps extends {} = {}, + StyleType extends ReactNativeStyle = ReactNativeStyle +> { + /** + * @typeparam AdditionalProps Additional props to add to your styled component + */ + ( + ...styles: ArrayInterpolation< + ComponentProps & + SpecificComponentProps & + AdditionalProps & { theme: Theme }, + StyleType + > + ): StyledComponent + /** + * @typeparam AdditionalProps Additional props to add to your styled component + */ + ( + template: TemplateStringsArray, + ...styles: ArrayInterpolation< + ComponentProps & + SpecificComponentProps & + AdditionalProps & { theme: Theme }, + StyleType + > + ): StyledComponent +} + +/** + * @desc + * This function accepts a React component. + * + * @example styled(MyComponent)({ width: 100 }) + * @example styled(MyComponent)(myComponentProps => ({ width: myComponentProps.width }) + * @example styled(View)({ width: 100 }) + * @example styled(View)(props => ({ width: props.width }) + */ +export interface CreateStyled { + < + Component extends ComponentType>, + ForwardedProps extends keyof ComponentPropsWithoutRef< + Component + > = keyof ComponentPropsWithoutRef + >( + component: Component, + options: FilteringStyledOptions< + ComponentPropsWithoutRef, + ForwardedProps + > + ): CreateStyledComponent< + Pick, ForwardedProps> & { + theme?: Theme + }, + {}, + ReactNativeStyleType> + > + + >>( + component: Component, + options?: StyledOptions> + ): CreateStyledComponent< + ComponentPropsWithoutRef & { theme?: Theme }, + {}, + ReactNativeStyleType> + > + + < + ComponentName extends ReactNativeComponentNames, + ForwardedProps extends keyof ReactNativeComponentProps< + ComponentName + > = keyof ReactNativeComponentProps + >( + component: ReactNativeComponents[ComponentName], + options: FilteringStyledOptions< + ReactNativeComponentProps, + ForwardedProps + > + ): CreateStyledComponent< + { theme?: Theme }, + Pick, ForwardedProps>, + ReactNativeStyleType> + > + + ( + component: ReactNativeComponents[ComponentName], + options?: StyledOptions> + ): CreateStyledComponent< + { theme?: Theme }, + ReactNativeComponentProps, + ReactNativeStyleType> + > +} + +export const styled: CreateStyled diff --git a/packages/native/types/index.d.ts b/packages/native/types/index.d.ts new file mode 100644 index 000000000..06174c001 --- /dev/null +++ b/packages/native/types/index.d.ts @@ -0,0 +1,54 @@ +// Definitions by: Pat Sissons +// TypeScript Version: 3.4 + +import { Theme } from '@emotion/core' + +import { + CreateStyled as BaseCreateStyled, + CreateStyledComponent, + CSSInterpolation, + Interpolation, + ReactNativeStyle, + ReactNativeComponentNames, + ReactNativeComponentProps, + ReactNativeComponents, + ReactNativeStyleType +} from './base' + +export { + ArrayCSSInterpolation, + ArrayInterpolation, + CreateStyledComponent, + CSSInterpolation, + FunctionInterpolation, + Interpolation, + InterpolationPrimitive, + ObjectInterpolation, + ReactNativeStyle, + StyledComponent, + StyledOptions +} from './base' + +export function css( + template: TemplateStringsArray, + ...args: Array +): StyleType +export function css( + ...args: Array +): StyleType +export function css( + ...args: Array +): StyleType + +export type StyledComponents = { + [ComponentName in ReactNativeComponentNames]: CreateStyledComponent< + { theme?: Theme }, + ReactNativeComponentProps, + ReactNativeStyleType> + > +} + +export interface CreateStyled extends BaseCreateStyled, StyledComponents {} + +declare const styled: CreateStyled +export default styled diff --git a/packages/native/types/tests.tsx b/packages/native/types/tests.tsx new file mode 100644 index 000000000..7333f85f3 --- /dev/null +++ b/packages/native/types/tests.tsx @@ -0,0 +1,158 @@ +import * as React from 'react' +import { + FlexStyle, + ImageStyle, + ScrollView, + StyleProp, + TextStyle, + View +} from 'react-native' +import styled, { css, ReactNativeStyle } from '@emotion/native' + +declare module '@emotion/core' { + // tslint:disable-next-line: strict-export-declare-modifiers + export interface Theme { + color: { + primary: string + positive: string + negative: string + } + } +} + +const cssObject = { + height: 100, + width: '100%', + display: 'flex', + position: undefined +} as const + +const className = css` + ${(true as boolean) && ''} + ${'bar'} + ${css``} + ${1} + ${cssObject} +` + +const className2: ReactNativeStyle = css(cssObject) + +css([{ display: 'none' }, [{ position: 'relative' }, { width: 100 }]]) + +css({ display: 'none' }, [{ position: 'relative' }, { width: 100 }]) + +css(null) + +interface ExtraProps { + foo: string +} + +interface AdditionalProps { + bar: string +} + +export const ExplicitExtraPropsView = styled.View` + background-color: red; // ${({ foo }) => foo} +` + +export const InferredPropsView = styled.View` + background-color: green; // ${({ testID }) => testID} +` + +export const InferredExtraPropsView = styled.View` + background-color: blue; // ${({ foo }) => foo} +` + +export const ThemedView = styled.View` + background-color: ${({ theme }) => theme.color.positive}; // ${({ bar }) => + bar} +` + +const largeTextStyle: TextStyle = { + fontSize: 24 +} + +const stretchImageStyle: ImageStyle = { + resizeMode: 'stretch' +} + +// for some reason, TypeScript is not complaining about the incorrect interpolated type +styled.Text(largeTextStyle, stretchImageStyle) +export const LargeText = styled.Text` + ${largeTextStyle} + // ${stretchImageStyle} +` + +styled.Image( + stretchImageStyle + // this style will not align with the ImageStyle typing requirement + // largeTextStyle +) +export const StretchedImage = styled.Image` + ${stretchImageStyle}; +` + +export const ComposedView = styled.View` + ${className} ${className2} + background-color: white; +` + +export const NestedComposedView = styled.View(css` + ${className} ${className2} + background-color: white; +`) + +function MyStyledComponent(_props: { style?: StyleProp }) { + return null +} + +styled(MyStyledComponent)(stretchImageStyle) + +const theme = { + color: { + primary: 'blue', + negative: 'red', + positive: 'green' + } +} + +export const themed = +export const composed = + +function MyComponent(_props: AdditionalProps) { + return null +} + +function MyOtherComponent(_props: { foo: string }) { + return null +} + +styled(MyComponent)({ width: 100 }) +styled(MyComponent)({ width: 100 }).withComponent(MyOtherComponent) +styled(MyComponent)(({ bar }) => ({ color: bar })) +styled(View)({ width: 100 }) +styled(View)(({ foo, testID }) => ({ color: foo, testID })) + +const styles = { + container: css({ flex: 1 }), + scrollContainer: css` + flex-grow: 1; + align-items: center; + `, + centered: css` + justify-content: center; + align-items: center; + ` +} + +export const scrollElem = ( + + + +) +export const Container = styled.View(styles.container) +export const CenterContainer = styled.View(styles.container, styles.centered) +export const ImageFullWidthContained = styled.Image` + ${styles.container} width: 100%; + resize-mode: contain; +` diff --git a/packages/native/types/tsconfig.json b/packages/native/types/tsconfig.json new file mode 100644 index 000000000..ea6734342 --- /dev/null +++ b/packages/native/types/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "baseUrl": "../", + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "lib": [ + "es6", + "dom" + ], + "module": "commonjs", + "noEmit": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strict": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "target": "es5", + "typeRoots": [ + "../" + ], + "types": [] + }, + "include": [ + "./*.ts", + "./*.tsx" + ] +} diff --git a/packages/native/types/tslint.json b/packages/native/types/tslint.json new file mode 100644 index 000000000..0ae058f36 --- /dev/null +++ b/packages/native/types/tslint.json @@ -0,0 +1,24 @@ +{ + "extends": "dtslint/dtslint.json", + "rules": { + "array-type": [true, "generic"], + "callable-types": false, + "import-spacing": false, + "semicolon": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-module", + "check-rest-spread", + "check-type", + "check-typecast", + "check-type-operator", + "check-preblock" + ], + "no-null-undefined-union": false, + "no-unnecessary-generics": false, + "strict-export-declare-modifiers": false + } +} diff --git a/yarn.lock b/yarn.lock index c9e09dbae..aa409b6dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3427,6 +3427,14 @@ dependencies: "@types/react" "*" +"@types/react-native@^0.57.0": + version "0.57.65" + resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.57.65.tgz#9da4773aaa95924bce42a54a5c19cfd8ffd5022b" + integrity sha512-7P5ulTb+/cnwbABWaAjzKmSYkRWeK7UCTfUwHhDpnwxdiL2X/KbdN1sPgo0B2E4zxfYE3MEoHv7FhB8Acfvf8A== + dependencies: + "@types/prop-types" "*" + "@types/react" "*" + "@types/react@*", "@types/react@^16.8.12", "@types/react@^16.8.6": version "16.8.24" resolved "https://registry.npmjs.org/@types/react/-/react-16.8.24.tgz#8d1ea1fcbfa214220da3d3c04e506f1077b0deac"