diff --git a/.yarnrc.yml b/.yarnrc.yml index 6f2a17b48d69..d9b0ec0726d2 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -44,6 +44,7 @@ npmPreapprovedPackages: - 'babel-plugin-syntax-hermes-parser' - 'hermes-parser' - 'hermes-estree' + - 'tstyche' tsEnableAutoTypes: false diff --git a/package.json b/package.json index fdb411ab2ffa..9fc81e7cbaa5 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "lint:workspaces": "yarn workspaces foreach --all --exclude \"react-native-reanimated-monorepo\" --parallel --topological-dev run lint", "lint:root": "eslint . --ignore-pattern 'packages/**' --ignore-pattern 'apps/**' --ignore-pattern 'docs/**' && yarn run --top-level oxfmt --check --ignore-path .prettiereslintignore .", "prepare-vscode": "bash ./scripts/prepare-vscode.sh", + "type:check:tstyche": "tstyche", "toggle-bundle-mode": "bash ./scripts/toggle-bundle-mode.sh" }, "devDependencies": { @@ -82,6 +83,7 @@ "remark-gfm": "4.0.1", "remark-mdx": "3.1.0", "shelljs": "0.10.0", + "tstyche": "7.2.1", "typescript": "5.9.3" }, "resolutions": { diff --git a/packages/react-native-reanimated/__typetests__/FeatureFlagTest.tst.ts b/packages/react-native-reanimated/__typetests__/FeatureFlagTest.tst.ts new file mode 100644 index 000000000000..dfee2d554656 --- /dev/null +++ b/packages/react-native-reanimated/__typetests__/FeatureFlagTest.tst.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from 'tstyche'; + +import { getStaticFeatureFlag } from '..'; + +describe('getStaticFeatureFlag', () => { + test('accepts a known static feature flag', () => { + expect(getStaticFeatureFlag).type.toBeCallableWith('RUNTIME_TEST_FLAG'); + }); + + test('rejects an unknown feature flag', () => { + expect(getStaticFeatureFlag).type.not.toBeCallableWith('NON_EXISTENT_FLAG'); + }); +}); diff --git a/packages/react-native-reanimated/__typetests__/FeatureFlagTest.tsx b/packages/react-native-reanimated/__typetests__/FeatureFlagTest.tsx deleted file mode 100644 index da7d1dcd12a1..000000000000 --- a/packages/react-native-reanimated/__typetests__/FeatureFlagTest.tsx +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { getStaticFeatureFlag } from '..'; - -function GetStaticFeatureFlagTest() { - // ok - this flag exists - getStaticFeatureFlag('RUNTIME_TEST_FLAG'); - - // @ts-expect-error - this flag does not exist - getStaticFeatureFlag('NON_EXISTENT_FLAG'); -} diff --git a/packages/react-native-reanimated/__typetests__/nativeMethodsTest.tst.ts b/packages/react-native-reanimated/__typetests__/nativeMethodsTest.tst.ts new file mode 100644 index 000000000000..705b8998658f --- /dev/null +++ b/packages/react-native-reanimated/__typetests__/nativeMethodsTest.tst.ts @@ -0,0 +1,66 @@ +import { useRef } from 'react'; +import { describe, expect, test } from 'tstyche'; + +import type Animated from '..'; +import { + dispatchCommand, + measure, + scrollTo, + setGestureState, + useAnimatedRef, +} from '..'; + +describe('measure', () => { + test('accepts an animated ref', () => { + const animatedRef = useAnimatedRef(); + expect(measure).type.toBeCallableWith(animatedRef); + }); + + test('rejects a plain ref', () => { + const plainRef = useRef(null); + expect(measure).type.not.toBeCallableWith(plainRef); + }); +}); + +describe('dispatchCommand', () => { + test('accepts an animated ref with a command and args', () => { + const animatedRef = useAnimatedRef(); + expect(dispatchCommand).type.toBeCallableWith( + animatedRef, + 'command', + [1, 2, 3] + ); + }); + + test('works without command arguments', () => { + const animatedRef = useAnimatedRef(); + expect(dispatchCommand).type.toBeCallableWith(animatedRef, 'command'); + }); + + test('rejects a plain ref', () => { + const plainRef = useRef(null); + expect(dispatchCommand).type.not.toBeCallableWith( + plainRef, + 'command', + [1, 2, 3] + ); + }); +}); + +describe('scrollTo', () => { + test('accepts an animated scroll view ref', () => { + const animatedRef = useAnimatedRef(); + expect(scrollTo).type.toBeCallableWith(animatedRef, 0, 0, true); + }); + + test('rejects a plain ref', () => { + const plainRef = useRef(null); + expect(scrollTo).type.not.toBeCallableWith(plainRef, 0, 0, true); + }); +}); + +describe('setGestureState', () => { + test('accepts a handler tag and a new state', () => { + expect(setGestureState).type.toBeCallableWith(1, 2); + }); +}); diff --git a/packages/react-native-reanimated/__typetests__/nativeMethodsTest.tsx b/packages/react-native-reanimated/__typetests__/nativeMethodsTest.tsx deleted file mode 100644 index 1afd7918e099..000000000000 --- a/packages/react-native-reanimated/__typetests__/nativeMethodsTest.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { useRef } from 'react'; - -import type Animated from '..'; -import { - dispatchCommand, - measure, - scrollTo, - setGestureState, - useAnimatedRef, -} from '..'; - -function NativeMethodsTest() { - function MeasureTest() { - const animatedRef = useAnimatedRef(); - measure(animatedRef); - const plainRef = useRef(null); - // @ts-expect-error it should only work for Animated refs - measure(plainRef); - } - - function DispatchCommandTest() { - const animatedRef = useAnimatedRef(); - dispatchCommand(animatedRef, 'command', [1, 2, 3]); - const plainRef = useRef(null); - // @ts-expect-error it should only work for Animated refs - dispatchCommand(plainRef, 'command', [1, 2, 3]); - // it should work without arguments - dispatchCommand(animatedRef, 'command'); - } - - function ScrollToTest() { - const animatedRef = useAnimatedRef(); - scrollTo(animatedRef, 0, 0, true); - const plainRef = useRef(null); - // @ts-expect-error it should only work for Animated refs - scrollTo(plainRef, 0, 0, true); - const animatedViewRef = useAnimatedRef(); - } - - function SetGestureStateTest() { - setGestureState(1, 2); - } -} diff --git a/packages/react-native-reanimated/__typetests__/useAnimatedReactionTest.tst.ts b/packages/react-native-reanimated/__typetests__/useAnimatedReactionTest.tst.ts new file mode 100644 index 000000000000..e427d2f4101f --- /dev/null +++ b/packages/react-native-reanimated/__typetests__/useAnimatedReactionTest.tst.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from 'tstyche'; + +import { useAnimatedReaction, useSharedValue } from '..'; + +describe('useAnimatedReaction', () => { + test('accepts a prepare and a react function', () => { + const sv = useSharedValue(0); + expect(useAnimatedReaction).type.toBeCallableWith( + () => sv.value, + (value: number) => { + console.log(value); + } + ); + }); + + test('accepts an optional dependency array', () => { + const sv = useSharedValue(0); + expect(useAnimatedReaction).type.toBeCallableWith( + () => sv.value, + (value: number) => { + console.log(value); + }, + [] + ); + }); + + test('react receives the previous result', () => { + const sv = useSharedValue(0); + expect(useAnimatedReaction).type.toBeCallableWith( + () => sv.value, + (value: number, previousResult: number | null) => { + console.log(value, previousResult); + } + ); + }); +}); diff --git a/packages/react-native-reanimated/__typetests__/useAnimatedReactionTest.tsx b/packages/react-native-reanimated/__typetests__/useAnimatedReactionTest.tsx deleted file mode 100644 index 89c1165086e1..000000000000 --- a/packages/react-native-reanimated/__typetests__/useAnimatedReactionTest.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { useState } from 'react'; - -import { useAnimatedReaction, useSharedValue } from '..'; - -function UseAnimatedReactionTest() { - const [state, setState] = useState(); - const sv = useSharedValue(0); - - useAnimatedReaction( - () => { - return sv.value; - }, - (value) => { - console.log(value); - } - ); - - useAnimatedReaction( - () => { - return sv.value; - }, - (value) => { - console.log(value); - }, - [] - ); - - useAnimatedReaction( - () => { - return sv.value; - }, - (value) => { - console.log(value); - }, - [state] - ); - - useAnimatedReaction( - () => { - return sv.value; - }, - (value, previousResult) => { - console.log(value, previousResult); - } - ); - - return null; -} diff --git a/packages/react-native-reanimated/package.json b/packages/react-native-reanimated/package.json index a7f77a67aa6f..740783b7028f 100644 --- a/packages/react-native-reanimated/package.json +++ b/packages/react-native-reanimated/package.json @@ -30,7 +30,7 @@ "type:check:src:native": "yarn tsc --noEmit", "type:check:src:web": "yarn tsc --noEmit --project tsconfig.web.json", "type:check:app": "yarn workspace common-app type:check", - "type:check:tests": "../../scripts/test-ts.sh __typetests__", + "type:check:tests": "../../scripts/test-ts.sh __typetests__ && yarn run --top-level type:check:tstyche packages/react-native-reanimated/__typetests__", "type:check:strict": "yarn type:check:strict:src && yarn type:check:strict:app", "type:check:strict:src": "yarn tsc --noEmit --customConditions react-native-strict-api", "type:check:strict:app": "yarn workspace common-app type:check:strict", diff --git a/scripts/test-ts.sh b/scripts/test-ts.sh index fd77ebbd8967..32dcbcedb54a 100755 --- a/scripts/test-ts.sh +++ b/scripts/test-ts.sh @@ -6,6 +6,10 @@ FILES=() for path in "${ARGS[@]}"; do if [ -d "$path" ]; then for file in "$path"/*.ts "$path"/*.tsx "$path"/*.d.ts; do + # Skip TSTyche type tests; those run via the `tstyche` runner. + case "$file" in + *.tst.ts | *.tst.tsx) continue ;; + esac if [ -f "$file" ]; then FILES+=("$file") fi diff --git a/tstyche.json b/tstyche.json new file mode 100644 index 000000000000..c7cf1352a6e1 --- /dev/null +++ b/tstyche.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://tstyche.org/schemas/config.json", + "testFileMatch": ["packages/*/__typetests__/**/*.tst.*"] +} diff --git a/yarn.lock b/yarn.lock index b5289d11df55..7f4c6033af6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28184,6 +28184,7 @@ __metadata: remark-gfm: "npm:4.0.1" remark-mdx: "npm:3.1.0" shelljs: "npm:0.10.0" + tstyche: "npm:7.2.1" typescript: "npm:5.9.3" languageName: unknown linkType: soft @@ -32033,6 +32034,20 @@ __metadata: languageName: node linkType: hard +"tstyche@npm:7.2.1": + version: 7.2.1 + resolution: "tstyche@npm:7.2.1" + peerDependencies: + typescript: ">=5.4" + peerDependenciesMeta: + typescript: + optional: true + bin: + tstyche: dist/bin.js + checksum: 10/42b21eededd6924b2cd271fb9747be50240b881e5a582d32a2e1bd2e575bd0d50d9a454d82e6ca58b7b6b500be02ac117d579d77f70da6c26dd522ca8f3e97a2 + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0"