From fbcba62dbe6af767499a42f210c9ac04d1769c6c Mon Sep 17 00:00:00 2001 From: Luna Wei Date: Fri, 16 Sep 2022 20:13:53 -0700 Subject: [PATCH] Move TypeScript declarations into react-native (#34614) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: ## Changelog [General] [Added] - Add `types` folder to house TypeScript types. Release TypesScript types with react-native and eventually deprecate [types/react-native](https://www.npmjs.com/package/types/react-native). The current plan is to release types/react-native for 0.70 and 0.71 while also maintaining types here. This will result in some double maintenance until 0.72 but will give community time to move off of types/react-native. After this lands, there have been changes on `main` of types that we need to update. Then, when we release 0.71 from DefinitelyTyped, we can simply copy over the `types` folder from this repo. Pull Request resolved: https://github.com/facebook/react-native/pull/34614 Test Plan: `yarn run test-typescript` for linting types * Created a new project using the TS template and my local clone of `react-native` on this branch. `npx react-native init MyTSApp --version --template react-native-template-typescript` * Updated the `package.json` to remove `types/react-native` * Deleted my node_modules and re-ran yarn * Opened MyTSApp in VSCode and verified the type suggestions appeared and cmd+click to defnitions took me to the node_module dependency `react-native/types` ## Danger is failing on this PR and it's expected as it runs off the changes on `main`. [This is expected](https://docs.github.com/en/github-ae@latest/actions/using-workflows/events-that-trigger-workflows?fbclid=IwAR2_AE0Jwndt8Gu-iTQnxGxLJq7nakbi7sz8jwZ6U62JWLSdcZuvjcQ6WvE#pull_request_target). However testing it locally passes. Once merged, and these changes are on `main`, danger will pass again. ``` $ react-native/packages/react-native-bots ❯ yarn danger pr https://github.com/facebook/react-native/pull/34614 yarn run v1.22.19 $ ..react-native/node_modules/.bin/danger pr https://github.com/facebook/react-native/pull/34614 Starting Danger PR on facebook/react-native#34614 Danger: ✓ found only warnings, not failing the build ## Warnings :lock: package.json - Changes were made to package.json. This will require a manual import by a Facebook employee. ✨ Done in 13.24s. ``` Reviewed By: mdvacca Differential Revision: D39479137 Pulled By: lunaleaps fbshipit-source-id: a7398492386abee9543b3bb5d98682cdf15f54da --- .circleci/config.yml | 5 + .eslintrc.js | 12 + package.json | 8 +- repo-config/package.json | 4 + scripts/.npmignore | 2 +- types/BatchedBridge.d.ts | 32 + types/Codegen.d.ts | 74 + types/Devtools.d.ts | 31 + types/LaunchScreen.d.ts | 18 + types/__typetests__/animated.tsx | 242 + .../__typetests__/fabric-component-sample.ts | 55 + types/__typetests__/globals.tsx | 178 + types/__typetests__/index.tsx | 2168 ++++ types/__typetests__/init-example.tsx | 117 + types/__typetests__/legacy-properties.tsx | 13 + types/__typetests__/stylesheet-create.tsx | 33 + types/__typetests__/stylesheet-flatten.tsx | 33 + types/__typetests__/turbo-module-sample.ts | 22 + types/globals.d.ts | 577 + types/index.d.ts | 10702 ++++++++++++++++ types/legacy-properties.d.ts | 245 + types/tsconfig.json | 19 + types/tslint.json | 23 + yarn.lock | 771 +- 24 files changed, 15357 insertions(+), 27 deletions(-) create mode 100644 types/BatchedBridge.d.ts create mode 100644 types/Codegen.d.ts create mode 100644 types/Devtools.d.ts create mode 100644 types/LaunchScreen.d.ts create mode 100644 types/__typetests__/animated.tsx create mode 100644 types/__typetests__/fabric-component-sample.ts create mode 100644 types/__typetests__/globals.tsx create mode 100644 types/__typetests__/index.tsx create mode 100644 types/__typetests__/init-example.tsx create mode 100644 types/__typetests__/legacy-properties.tsx create mode 100644 types/__typetests__/stylesheet-create.tsx create mode 100644 types/__typetests__/stylesheet-flatten.tsx create mode 100644 types/__typetests__/turbo-module-sample.ts create mode 100644 types/globals.d.ts create mode 100644 types/index.d.ts create mode 100644 types/legacy-properties.d.ts create mode 100644 types/tsconfig.json create mode 100644 types/tslint.json diff --git a/.circleci/config.yml b/.circleci/config.yml index 2cc6315c5fc3ee..3c22a29dcdc50a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -345,6 +345,11 @@ jobs: command: yarn flow-check-android when: always + - run: + name: Run TypeScript tests + command: yarn test-typescript + when: always + - run: name: Sanity checks command: | diff --git a/.eslintrc.js b/.eslintrc.js index d07d0d884c0b5c..c2adaccf1a2edc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -71,5 +71,17 @@ module.exports = { jest: true, }, }, + { + files: ['types/**/*.{ts,tsx}'], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint/eslint-plugin'], + rules: { + '@typescript-eslint/no-unused-vars': 'off', + 'react-native/no-inline-styles': 'off', + '@typescript-eslint/no-shadow': 'off', + 'no-self-compare': 'off', + 'react/self-closing-comp': 'off', + }, + }, ], }; diff --git a/package.json b/package.json index 31048428246916..d06f635855a182 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "engines": { "node": ">=14" }, + "types": "types", "jest-junit": { "outputDirectory": "reports/junit", "outputName": "js-test-results.xml" @@ -81,8 +82,8 @@ "shellcheck": "./scripts/circleci/analyze_scripts.sh", "clang-format": "clang-format -i --glob=*/**/*.{h,cpp,m,mm}", "format": "npm run prettier && npm run clang-format", - "prettier": "prettier --write \"./**/*.{js,md,yml}\"", - "format-check": "prettier --list-different \"./**/*.{js,md,yml}\"", + "prettier": "prettier --write \"./**/*.{js,md,yml,ts,tsx}\"", + "format-check": "prettier --list-different \"./**/*.{js,md,yml,ts,tsx}\"", "update-lock": "npx yarn-deduplicate", "docker-setup-android": "docker pull reactnativecommunity/react-native-android:5.2", "docker-build-android": "docker build -t reactnativeci/android -f .circleci/Dockerfiles/Dockerfile.android .", @@ -93,7 +94,8 @@ "test-android-instrumentation": "yarn run docker-build-android && yarn run test-android-run-instrumentation", "test-android-unit": "yarn run docker-build-android && yarn run test-android-run-unit", "test-android-e2e": "yarn run docker-build-android && yarn run test-android-run-e2e", - "test-ios": "./scripts/objc-test.sh test" + "test-ios": "./scripts/objc-test.sh test", + "test-typescript": "dtslint types" }, "workspaces": [ "packages/*", diff --git a/repo-config/package.json b/repo-config/package.json index 596c3a1b0eeda8..b4fdb07316b411 100644 --- a/repo-config/package.json +++ b/repo-config/package.json @@ -13,9 +13,12 @@ "@babel/eslint-parser": "^7.18.2", "@babel/generator": "^7.14.0", "@babel/plugin-transform-regenerator": "^7.0.0", + "@definitelytyped/dtslint": "^0.0.127", "@react-native-community/eslint-plugin": "*", "@react-native/eslint-plugin-specs": "^0.71.0", "@reactions/component": "^2.0.2", + "@types/react": "^18.0.18", + "@typescript-eslint/parser": "^5.30.5", "async": "^3.2.2", "clang-format": "^1.8.0", "connect": "^3.6.5", @@ -46,6 +49,7 @@ "react-test-renderer": "18.2.0", "shelljs": "^0.8.5", "signedsource": "^1.0.0", + "typescript": "4.1.3", "ws": "^6.1.4", "yargs": "^17.5.1" }, diff --git a/scripts/.npmignore b/scripts/.npmignore index ee653fdf1e65c8..6f5224b226432e 100644 --- a/scripts/.npmignore +++ b/scripts/.npmignore @@ -1,2 +1,2 @@ # Make sure we never publish __test__ folders (Gradle output) -**/__tests__/ +**/__*tests__/ diff --git a/types/BatchedBridge.d.ts b/types/BatchedBridge.d.ts new file mode 100644 index 00000000000000..c6b2e145a2f38a --- /dev/null +++ b/types/BatchedBridge.d.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +interface SpyData { + type: number; + module?: string | undefined; + method: string | number; + args: any[]; +} + +declare class MessageQueue { + static spy(spyOrToggle: boolean | ((data: SpyData) => void)): void; + + getCallableModule(name: string): Object; + registerCallableModule(name: string, module: Object): void; + registerLazyCallableModule(name: string, factory: () => Object): void; +} + +declare module 'react-native/Libraries/BatchedBridge/BatchedBridge' { + const BatchedBridge: MessageQueue; + export default BatchedBridge; +} + +declare module 'react-native/Libraries/BatchedBridge/MessageQueue' { + export default MessageQueue; +} diff --git a/types/Codegen.d.ts b/types/Codegen.d.ts new file mode 100644 index 00000000000000..24de3b099159cc --- /dev/null +++ b/types/Codegen.d.ts @@ -0,0 +1,74 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +declare module 'react-native/Libraries/Utilities/codegenNativeCommands' { + export interface Options { + readonly supportedCommands: ReadonlyArray; + } + + function codegenNativeCommands( + options: Options, + ): T; + + export default codegenNativeCommands; +} + +declare module 'react-native/Libraries/Utilities/codegenNativeComponent' { + import type {HostComponent} from 'react-native'; + + export interface Options { + readonly interfaceOnly?: boolean; + readonly paperComponentName?: string; + readonly paperComponentNameDeprecated?: string; + readonly excludedPlatforms?: ReadonlyArray<'iOS' | 'android'>; + } + + export type NativeComponentType = HostComponent; + + function codegenNativeComponent( + componentName: string, + options?: Options, + ): NativeComponentType; + + export default codegenNativeComponent; +} + +declare module 'react-native/Libraries/Types/CodegenTypes' { + import type {NativeSyntheticEvent} from 'react-native'; + + // Event types + // We're not using the PaperName, it is only used to codegen view config settings + + export type BubblingEventHandler< + T, + PaperName extends string | never = never, + > = (event: NativeSyntheticEvent) => void | Promise; + export type DirectEventHandler< + T, + PaperName extends string | never = never, + > = (event: NativeSyntheticEvent) => void | Promise; + + // Prop types + export type Double = number; + export type Float = number; + export type Int32 = number; + export type UnsafeObject = object; + + type DefaultTypes = number | boolean | string | ReadonlyArray; + // Default handling, ignore the unused value + // we're only using it for type checking + // + // TODO: (rickhanlonii) T44881457 If a default is provided, it should always be optional + // but that is currently not supported in the codegen since we require a default + + export type WithDefault< + Type extends DefaultTypes, + Value extends Type | string | undefined | null, + > = Type | undefined | null; +} diff --git a/types/Devtools.d.ts b/types/Devtools.d.ts new file mode 100644 index 00000000000000..3a993cf7ab7b5b --- /dev/null +++ b/types/Devtools.d.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +declare module 'react-native/Libraries/Core/Devtools/parseErrorStack' { + export type StackFrame = { + file: string; + methodName: string; + lineNumber: number; + column: number | null; + }; + + export interface ExtendedError extends Error { + framesToPop?: number | undefined; + } + + export default function parseErrorStack(error: ExtendedError): StackFrame[]; +} + +declare module 'react-native/Libraries/Core/Devtools/symbolicateStackTrace' { + import {StackFrame} from 'react-native/Libraries/Core/Devtools/parseErrorStack'; + + export default function symbolicateStackTrace( + stack: ReadonlyArray, + ): Promise; +} diff --git a/types/LaunchScreen.d.ts b/types/LaunchScreen.d.ts new file mode 100644 index 00000000000000..3e816dd065274e --- /dev/null +++ b/types/LaunchScreen.d.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +// Adds the JSX elements used in the launch screen. + +declare module 'react-native/Libraries/NewAppScreen' { + export const Header: any; + export const LearnMoreLinks: any; + export const Colors: any; + export const DebugInstructions: any; + export const ReloadInstructions: any; +} diff --git a/types/__typetests__/animated.tsx b/types/__typetests__/animated.tsx new file mode 100644 index 00000000000000..bee061fb72d2e9 --- /dev/null +++ b/types/__typetests__/animated.tsx @@ -0,0 +1,242 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import * as React from 'react'; + +import { + Animated, + View, + NativeSyntheticEvent, + NativeScrollEvent, + StyleProp, + SectionListData, +} from 'react-native'; + +interface CompProps { + width: number; +} + +class Comp extends React.Component { + f1: () => boolean = () => true; + + render() { + const {width} = this.props; + return ; + } +} + +const ForwardComp = React.forwardRef(({width}, ref) => { + function f1(): boolean { + return true; + } + + return ; +}); + +type X = React.PropsWithoutRef>; + +type Props = React.ComponentPropsWithRef; +const AnimatedWrapperComponent: React.FunctionComponent = ({ + key, // $ExpectType string | number | null | undefined || Key | null | undefined + ...props +}) => ; + +function TestAnimatedAPI() { + // Value + const v1 = new Animated.Value(0); + const v2 = new Animated.Value(0); + + // Ref + const AnimatedViewRef = React.useRef(null); + + AnimatedViewRef.current && + AnimatedViewRef.current.measure(() => { + return; + }); + + const AnimatedComp = Animated.createAnimatedComponent(Comp); + + const AnimatedCompRef = React.useRef(null); + + AnimatedCompRef.current && AnimatedCompRef.current.f1(); + + const AnimatedForwardComp = Animated.createAnimatedComponent(ForwardComp); + + const AnimatedForwardCompRef = + React.useRef>(null); + const ForwardCompRef = React.useRef(null); + + AnimatedForwardCompRef.current && + AnimatedForwardCompRef.current.measure(() => { + return; + }); + + v1.setValue(0.1); + + v1.addListener(e => { + const n: number = e.value; + }); + + const v200 = v1.interpolate({ + inputRange: [0, 1], + outputRange: [0, 200], + }); + + const id = v200.addListener(() => {}); + v200.removeListener(id); + v200.removeAllListeners(); + v200.hasListeners(); + + Animated.timing(v2, { + toValue: v1.interpolate({inputRange: [0, 1], outputRange: [0, 200]}), + useNativeDriver: false, + }); + + // ValueXY + const position = new Animated.ValueXY({x: 0, y: 0}); + + // Animation functions + const spring1 = Animated.spring(v1, { + toValue: 0.5, + tension: 10, + delay: 100, + useNativeDriver: false, + }); + + const springXY = Animated.spring(position, { + toValue: { + x: 1, + y: 2, + }, + useNativeDriver: false, + }); + + spring1.start(); + spring1.stop(); + spring1.reset(); + + Animated.parallel( + [ + Animated.spring(v1, {toValue: 1, useNativeDriver: false}), + Animated.spring(v2, {toValue: 1, useNativeDriver: false}), + ], + { + stopTogether: true, + }, + ); + + Animated.decay(v1, { + velocity: 2, + useNativeDriver: false, + }); + + Animated.timing(v1, { + toValue: 1, + duration: 100, + delay: 100, + easing: v => v, + useNativeDriver: false, + }); + + Animated.add(v1, v2); + Animated.subtract(v1, v2); + Animated.divide(v1, v2); + Animated.multiply(v1, v2); + Animated.modulo(v1, 2); + + Animated.delay(100); + + Animated.sequence([spring1, springXY]); + + Animated.stagger(100, [spring1, springXY]); + + const listener = (e?: NativeSyntheticEvent) => { + if (e) { + console.warn(e.nativeEvent.contentOffset.y); + } + }; + + Animated.event([{nativeEvent: {contentOffset: {y: v1}}}], { + useNativeDriver: true, + listener, + }); + + const AnimatedView = Animated.createAnimatedComponent(View); + const ref = React.useRef(null); + const legacyRef = React.useRef>(null); + + return ( + + + + i has children + + + + + + + + { + const x = event.nativeEvent.layout.x; // $ExpectType number + const y = event.nativeEvent.layout.y; // $ExpectType number + const width = event.nativeEvent.layout.width; // $ExpectType number + const height = event.nativeEvent.layout.height; // $ExpectType number + }} + /> + ; + } + renderItem={info => { + info; // $ExpectType ListRenderItemInfo + return ; + }} + /> + ; + [] + } + renderItem={info => { + /* + * Original expects: + * SectionListRenderItemInfo on TS@3.5, + * SectionListRenderItemInfo on TS@4.0. + * Skip until original is adjusted and type can be asserted + */ + info; // Should expect SectionListRenderItemInfo + info.section.title; // $ExpectType string + return ; + }} + /> + ; + + ); +} diff --git a/types/__typetests__/fabric-component-sample.ts b/types/__typetests__/fabric-component-sample.ts new file mode 100644 index 00000000000000..73d91a6bcee8ee --- /dev/null +++ b/types/__typetests__/fabric-component-sample.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import codegenNativeComponent, { + NativeComponentType, +} from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; +import { + WithDefault, + Double, + Float, + Int32, + UnsafeObject, + BubblingEventHandler, + DirectEventHandler, +} from 'react-native/Libraries/Types/CodegenTypes'; +import type {ViewProps} from 'react-native'; + +type Event = Readonly<{ + value: Double; +}>; + +interface NativeProps extends ViewProps { + string?: string; + number?: number; + boolean?: boolean; + default?: WithDefault<'option1' | 'option2', 'option1'>; + double?: Double; + float?: Float; + int32?: Int32; + unsafeObject?: UnsafeObject; + onBubblingEventHandler?: BubblingEventHandler; + onDirectEventHandler?: DirectEventHandler; +} + +export type SampleViewType = NativeComponentType; + +interface NativeCommands { + changeBackgroundColor: ( + viewRef: React.ElementRef, + color: string, + ) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: ['changeBackgroundColor'], +}); + +export default codegenNativeComponent('SampleView'); diff --git a/types/__typetests__/globals.tsx b/types/__typetests__/globals.tsx new file mode 100644 index 00000000000000..e14a8029d6da27 --- /dev/null +++ b/types/__typetests__/globals.tsx @@ -0,0 +1,178 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const noop = () => { }; + +function testInterval() { + let handle = setInterval(noop, 0); + clearInterval(handle); + + handle = setInterval((arg1: number, arg2: string) => { + console.log('arg1', arg1); + console.log('arg2', arg2); + }, 0, 100, '200'); + clearInterval(handle); + + handle = setInterval((arg1: number, arg2: string) => { + console.log('arg1', arg1); + console.log('arg2', arg2); + // @ts-expect-error + }, 0, 'wrong-type', '200'); + clearInterval(handle); + + // @ts-expect-error + handle = setInterval((missingArg: any) => { + console.log('missingArg', missingArg); + }, 0); + clearInterval(handle); + + handle = setInterval((arg1: number) => { + console.log('arg1', arg1); + // @ts-expect-error + }, 0, 100, 'missing-arg'); + clearInterval(handle); +} + +function testTimeout() { + let handle = setTimeout(noop, 0); + clearTimeout(handle); + + handle = setTimeout((arg1: number, arg2: string) => { + console.log('arg1', arg1); + console.log('arg2', arg2); + }, 0, 100, '200'); + clearTimeout(handle); + + handle = setTimeout((arg1: number, arg2: string) => { + console.log('arg1', arg1); + console.log('arg2', arg2); + // @ts-expect-error + }, 0, 'wrong-type', '200'); + clearTimeout(handle); + + // @ts-expect-error + handle = setTimeout((missingArg: any) => { + console.log('missingArg', missingArg); + }, 0); + clearTimeout(handle); + + handle = setTimeout((arg1: number) => { + console.log('arg1', arg1); + // @ts-expect-error + }, 0, 100, 'missing-arg'); + clearTimeout(handle); +} + +function testImmediate() { + let handle = setImmediate(noop); + clearImmediate(handle); + + handle = setImmediate((arg1: number, arg2: string) => { + console.log('arg1', arg1); + console.log('arg2', arg2); + }, 100, '200'); + clearImmediate(handle); + + handle = setImmediate((arg1: number, arg2: string) => { + console.log('arg1', arg1); + console.log('arg2', arg2); + // @ts-expect-error + }, 'wrong-type', '200'); + clearImmediate(handle); + + // @ts-expect-error + handle = setImmediate((missingArg: any) => { + console.log('missingArg', missingArg); + }); + clearImmediate(handle); + + handle = setImmediate((arg1: number) => { + console.log('arg1', arg1); + // @ts-expect-error + }, 100, 'missing-arg'); + clearImmediate(handle); +} + +const fetchCopy: WindowOrWorkerGlobalScope['fetch'] = fetch; + +const myHeaders = new Headers(); +myHeaders.append('Content-Type', 'image/jpeg'); + +const myInit: RequestInit = { + method: 'GET', + headers: myHeaders, + mode: 'cors', + signal: new AbortSignal(), +}; + +const myRequest = new Request('flowers.jpg'); + +fetch(myRequest, myInit) + .then(response => { + console.log(response.type); + console.log(response.url); + console.log(response.status); + console.log(response.ok); + console.log(response.statusText); + console.log(response.headers); + + return response.blob(); + }) + .then(blob => { + const init = { status: 200, statusText: 'SuperSmashingGreat!' }; + const myResponse = new Response(blob, init); + }); + +const xmlRequest = new XMLHttpRequest(); + +xmlRequest.addEventListener('load', ev => { + console.log(ev.lengthComputable); + console.log(ev.loaded); + console.log(ev.total); +}); + +const test = new URLSearchParams(); + +const url = new URL('path', 'http://localhost/'); + +const blobA = new Blob(); +const textA = 'i \u2665 dogs'; + +const blob = new Blob([blobA, textA]); + +const reader = new FileReader(); + +reader.onloadend = ev => { + console.log(ev.target); + console.log(ev.loaded); +}; + +reader.readAsText(new Blob()); + +fetch('https://example.org/post-image', { + body: { uri: 'file:///data/tmp/qwerad3.jpg' }, + headers: { + 'Content-Type': 'type', + }, + method: 'POST', +}); + +const socket = new WebSocket('wss://echo.websocket.org'); +socket.send('hello world'); +socket.addEventListener('open', () => console.log('open')); +socket.onopen = () => console.log('open'); +socket.addEventListener('close', e => console.log(e.code)); +socket.onclose = e => console.log(e.code); +socket.addEventListener('message', e => console.log(e.data)); +socket.onmessage = e => console.log(e.data); +socket.addEventListener('error', e => console.log(e.message)); +socket.onerror = e => console.log(e.message); + +const formData = new FormData(); +formData.append('file', { fileName: 'example' }); +console.log(formData.getParts()); +console.log(formData.getAll()); diff --git a/types/__typetests__/index.tsx b/types/__typetests__/index.tsx new file mode 100644 index 00000000000000..b735c71f82de37 --- /dev/null +++ b/types/__typetests__/index.tsx @@ -0,0 +1,2168 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +/* +The content of index.io.js could be something like + + 'use strict'; + + import { AppRegistry } from 'react-native' + import Welcome from './gen/Welcome' + + AppRegistry.registerComponent('MopNative', () => Welcome); + +For a list of complete Typescript examples: check https://github.com/bgrieder/RNTSExplorer +*/ + +import * as PropTypes from 'prop-types'; +import * as React from 'react'; +import { + AccessibilityInfo, + ActionSheetIOS, + AsyncStorage, + Alert, + AppState, + AppStateStatus, + Appearance, + BackHandler, + Button, + ColorValue, + DataSourceAssetCallback, + DatePickerAndroid, + DevSettings, + DeviceEventEmitter, + DeviceEventEmitterStatic, + Dimensions, + DrawerLayoutAndroid, + DrawerSlideEvent, + DynamicColorIOS, + FlatList, + FlatListProps, + GestureResponderEvent, + HostComponent, + I18nManager, + Image, + ImageBackground, + ImageErrorEventData, + ImageLoadEventData, + ImageResizeMode, + ImageResolvedAssetSource, + ImageStyle, + InputAccessoryView, + InteractionManager, + Keyboard, + KeyboardAvoidingView, + LayoutChangeEvent, + Linking, + ListRenderItemInfo, + ListView, + ListViewDataSource, + LogBox, + MaskedViewIOS, + Modal, + NativeEventEmitter, + NativeModule, // Not actually exported, not sure why + NativeModules, + NativeScrollEvent, + NativeSyntheticEvent, + PermissionsAndroid, + Platform, + PlatformColor, + Pressable, + ProgressBarAndroid, + ProgressViewIOS, + PushNotificationIOS, + RefreshControl, + RegisteredStyle, + ScaledSize, + ScrollView, + ScrollViewProps, + SectionList, + SectionListProps, + SectionListRenderItemInfo, + Share, + ShareDismissedAction, + ShareSharedAction, + StatusBar, + StyleProp, + StyleSheet, + Switch, + SwitchIOS, + SwitchChangeEvent, + Systrace, + TabBarIOS, + Text, + TextInput, + TextInputChangeEventData, + TextInputContentSizeChangeEventData, + TextInputEndEditingEventData, + TextInputFocusEventData, + TextInputKeyPressEventData, + TextInputScrollEventData, + TextInputSelectionChangeEventData, + TextInputSubmitEditingEventData, + TextLayoutEventData, + TextProps, + TextStyle, + TimePickerAndroid, + TouchableNativeFeedback, + UIManager, + View, + ViewPagerAndroid, + ViewStyle, + VirtualizedList, + YellowBox, + findNodeHandle, + requireNativeComponent, + useColorScheme, + useWindowDimensions, + SectionListData, + ToastAndroid, + Touchable, + LayoutAnimation, +} from 'react-native'; + +declare module 'react-native' { + interface NativeTypedModule { + someFunction(): void; + someProperty: string; + } + interface NativeModulesStatic { + NativeTypedModule: NativeTypedModule; + } +} + +NativeModules.NativeUntypedModule; + +NativeModules.NativeTypedModule.someFunction(); +NativeModules.NativeTypedModule.someProperty = ''; + +function dimensionsListener(dimensions: { + window: ScaledSize; + screen: ScaledSize; +}) { + console.log('window dimensions: ', dimensions.window); + console.log('screen dimensions: ', dimensions.screen); +} + +function testDimensions() { + const {width, height, scale, fontScale} = Dimensions.get( + 1 === 1 ? 'window' : 'screen', + ); + + const subscription = Dimensions.addEventListener( + 'change', + dimensionsListener, + ); + subscription.remove(); +} + +function TextUseWindowDimensions() { + const {width, height, scale, fontScale} = useWindowDimensions(); +} + +BackHandler.addEventListener('hardwareBackPress', () => true).remove(); +BackHandler.addEventListener('hardwareBackPress', () => false).remove(); +BackHandler.addEventListener('hardwareBackPress', () => undefined).remove(); +BackHandler.addEventListener('hardwareBackPress', () => null).remove(); + +interface LocalStyles { + container: ViewStyle; + welcome: TextStyle; + instructions: TextStyle; +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#F5FCFF', + }, + welcome: { + fontSize: 20, + textAlign: 'center', + margin: 10, + }, + instructions: { + textAlign: 'center', + color: '#333333', + marginBottom: 5, + }, +}); + +//alternative declaration of styles (inline typings) +const stylesAlt = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#F5FCFF', + }, + welcome: { + fontSize: 20, + textAlign: 'center', + margin: 10, + }, + instructions: { + textAlign: 'center', + color: '#333333', + marginBottom: 5, + }, +}); + +StyleSheet.setStyleAttributePreprocessor( + 'fontFamily', + (family: string) => family, +); + +const welcomeFontSize = StyleSheet.flatten(styles.welcome).fontSize; + +const viewStyle: StyleProp = { + backgroundColor: '#F5FCFF', +}; +const textStyle: StyleProp = { + fontSize: 20, +}; +const imageStyle: StyleProp = { + resizeMode: 'contain', +}; +const fontVariantStyle: StyleProp = { + fontVariant: ['tabular-nums'], +}; + +const viewProperty = StyleSheet.flatten(viewStyle).backgroundColor; +const textProperty = StyleSheet.flatten(textStyle).fontSize; +const imageProperty = StyleSheet.flatten(imageStyle).resizeMode; +const fontVariantProperty = StyleSheet.flatten(fontVariantStyle).fontVariant; + +// correct use of the StyleSheet.flatten +const styleArray: StyleProp[] = []; +const flattenStyle = StyleSheet.flatten(styleArray); +const {top} = flattenStyle; + +const s = StyleSheet.create({ + shouldWork: { + fontWeight: '900', // if we comment this line, errors gone + marginTop: 5, // if this line commented, errors also gone + }, +}); +const f1: TextStyle = s.shouldWork; + +// StyleSheet.compose +// It creates a new style object by composing two existing styles +const composeTextStyle: StyleProp = { + color: '#000000', + fontSize: 20, +}; + +const composeImageStyle: StyleProp = { + resizeMode: 'contain', +}; + +// The following use of the compose method is valid +const combinedStyle: StyleProp = StyleSheet.compose( + composeTextStyle, + composeTextStyle, +); + +const combinedStyle1: StyleProp = StyleSheet.compose( + composeImageStyle, + composeImageStyle, +); + +const combinedStyle2: StyleProp> = + StyleSheet.compose([composeTextStyle], [composeTextStyle]); + +const combinedStyle3: StyleProp = StyleSheet.compose( + composeTextStyle, + null, +); + +const combinedStyle4: StyleProp | null> = + StyleSheet.compose([composeTextStyle], null); + +const combinedStyle5: StyleProp = StyleSheet.compose( + composeTextStyle, + Math.random() < 0.5 ? composeTextStyle : null, +); + +const combinedStyle6: StyleProp = StyleSheet.compose( + null, + null, +); + +// The following use of the compose method is invalid: +// @ts-expect-error +const combinedStyle7 = StyleSheet.compose(composeImageStyle, composeTextStyle); + +// @ts-expect-error +const combinedStyle8: StyleProp = StyleSheet.compose( + composeTextStyle, + composeTextStyle, +); + +// @ts-expect-error +const combinedStyle9: StyleProp = StyleSheet.compose( + [composeTextStyle], + null, +); + +// @ts-expect-error +const combinedStyle10: StyleProp = StyleSheet.compose( + Math.random() < 0.5 ? composeTextStyle : null, + null, +); + +const testNativeSyntheticEvent = ( + e: NativeSyntheticEvent, +): void => { + e.isDefaultPrevented(); + e.preventDefault(); + e.isPropagationStopped(); + e.stopPropagation(); + e.persist(); + e.cancelable; + e.bubbles; + e.currentTarget; + e.defaultPrevented; + e.eventPhase; + e.isTrusted; + e.nativeEvent; + e.target; + e.timeStamp; + e.type; + e.nativeEvent; +}; + +function eventHandler(e: T) {} + +function handler(e: GestureResponderEvent) { + eventHandler(e); +} + +type ElementProps = C extends React.Component ? P : never; + +class CustomView extends React.Component { + render() { + return ( + + Custom View + + ); + } +} + +class Welcome extends React.Component & {color: string}> { + rootViewRef = React.useRef(null); + customViewRef = React.useRef(null); + + testNativeMethods() { + if (this.rootViewRef.current != null) { + this.rootViewRef.current.setNativeProps({}); + this.rootViewRef.current.measure( + (x: number, y: number, width: number, height: number) => {}, + ); + } + } + + testFindNodeHandle() { + if (this.rootViewRef.current != null) { + const nativeComponentHandle = findNodeHandle(this.rootViewRef.current); + } + + if (this.customViewRef.current != null) { + const customComponentHandle = findNodeHandle(this.customViewRef.current); + const fromHandle = findNodeHandle(customComponentHandle); + } + } + + render() { + const {color, ...props} = this.props; + return ( + + Welcome to React Native + + To get started, edit index.ios.js + + + Press Cmd+R to reload,{'\n'} + Cmd+D or shake for dev menu + + + + ); + } +} + +export default Welcome; + +// TouchableTest +function TouchableTest() { + function basicUsage() { + if (Touchable.TOUCH_TARGET_DEBUG) { + return Touchable.renderDebugView({ + color: 'mediumspringgreen', + hitSlop: {bottom: 5, top: 5}, + }); + } + } + + function defaultHitSlop() { + return Touchable.renderDebugView({ + color: 'red', + }); + } +} + +// TouchableNativeFeedbackTest +export class TouchableNativeFeedbackTest extends React.Component { + onPressButton = (e: GestureResponderEvent) => { + e.persist(); + e.isPropagationStopped(); + e.isDefaultPrevented(); + }; + + render() { + return ( + <> + + + Button + + + + + Button + + + + + Button + + + + + Button + + + + + Button + + + + + Button + + + + + Button + + + + ); + } +} + +// PressableTest +export class PressableTest extends React.Component<{}> { + private readonly myRef: React.RefObject = React.createRef(); + + onPressButton = (e: GestureResponderEvent) => { + e.persist(); + e.isPropagationStopped(); + e.isDefaultPrevented(); + }; + + render() { + return ( + <> + + + Button + + + {/* Style function */} + ({ + backgroundColor: state.pressed ? 'red' : 'blue', + })}> + + Button + + + {/* Children function */} + ({ + backgroundColor: state.pressed ? 'red' : 'blue', + })}> + {state => + state.pressed ? ( + + Pressed + + ) : ( + + Not Pressed + + ) + } + + {/* Android Ripple */} + + + Button + + + + ); + } +} + +// App State +function appStateListener(state: string) { + console.log('New state: ' + state); +} + +function appStateTest() { + console.log('Current state: ' + AppState.currentState); + AppState.addEventListener('change', appStateListener); + AppState.addEventListener('blur', appStateListener); + AppState.addEventListener('focus', appStateListener); +} + +let appState: AppStateStatus = 'active'; +appState = 'background'; +appState = 'inactive'; +appState = 'unknown'; +appState = 'extension'; + +const AppStateExample = () => { + const appState = React.useRef(AppState.currentState); + const [appStateVisible, setAppStateVisible] = React.useState( + appState.current, + ); + const appStateIsAvailable = AppState.isAvailable; + + React.useEffect(() => { + const subscription = AppState.addEventListener('change', nextAppState => { + if ( + appState.current.match(/inactive|background/) && + nextAppState === 'active' + ) { + console.log('App has come to the foreground!'); + } + + appState.current = nextAppState; + setAppStateVisible(appState.current); + console.log('AppState', appState.current); + }); + + return () => { + subscription.remove(); + }; + }, []); + + return ( + + Current state is: {appStateVisible} + Available: {appStateIsAvailable} + + ); +}; + +// ViewPagerAndroid +export class ViewPagerAndroidTest { + render() { + return ( + { + console.log(`position: ${e.nativeEvent.position}`); + console.log(`offset: ${e.nativeEvent.offset}`); + }} + onPageSelected={e => { + console.log(`position: ${e.nativeEvent.position}`); + }} + /> + ); + } +} + +const profiledJSONParse = Systrace.measure('JSON', 'parse', JSON.parse); +profiledJSONParse('[]'); + +InteractionManager.runAfterInteractions(() => { + // ... +}).then(() => 'done'); + +export class FlatListTest extends React.Component, {}> { + list: FlatList | null = null; + + componentDidMount(): void { + if (this.list) { + this.list.flashScrollIndicators(); + } + } + + _renderItem = (rowData: any) => { + return ( + + {rowData.item} + + ); + }; + _cellRenderer = ({children}: any) => { + return {children}; + }; + + _renderSeparator = () => ( + + ); + + render() { + return ( + (this.list = list)} + data={[1, 2, 3, 4, 5]} + renderItem={this._renderItem} + ItemSeparatorComponent={this._renderSeparator} + ListFooterComponent={null} + ListFooterComponentStyle={[ + {padding: 8}, + [{backgroundColor: 'transparent'}], + ]} + ListHeaderComponent={null} + ListHeaderComponentStyle={[ + {padding: 8}, + [{backgroundColor: 'transparent'}], + ]} + CellRendererComponent={this._cellRenderer} + fadingEdgeLength={200} + /> + ); + } +} + +export class SectionListTest extends React.Component< + SectionListProps, + {} +> { + myList: React.RefObject>; + + constructor(props: SectionListProps) { + super(props); + this.myList = React.createRef(); + } + + scrollMe = () => { + this.myList.current && + this.myList.current.scrollToLocation({itemIndex: 0, sectionIndex: 1}); + }; + + render() { + const sections = [ + { + title: 'Section 1', + data: ['A', 'B', 'C', 'D', 'E'], + }, + { + title: 'Section 2', + data: ['A2', 'B2', 'C2', 'D2', 'E2'], + renderItem: (info: {item: string}) => ( + + {info.item} + + ), + }, + ]; + + const cellRenderer = ({children}: any) => { + return {children}; + }; + + return ( + +