From 9bd900e5f3673ae0ccef08528610374f25b7d3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Fri, 29 May 2026 14:51:29 +0200 Subject: [PATCH 1/7] Set up TSTyche type tests and migrate FeatureFlag test The __typetests__ suites assert types through tsc failures and ~220 `@ts-expect-error` directives, which match any error on the following line and give no per-test reporting. Start migrating them to TSTyche, which runs on the project's own TypeScript and produces named, isolated assertions. - Add the `tstyche` dev dependency and a `type:check:tstyche` script. - Add tstyche.config.json scoped to packages/*/__typetests__. - Convert FeatureFlagTest.tsx to FeatureFlagTest.tst.ts as the first test. - Make scripts/test-ts.sh skip *.tst.* so the old and new harnesses run side by side and files can be migrated one at a time. Builds on the TSTyche proof of concept from #7727. --- package.json | 2 ++ .../__typetests__/FeatureFlagTest.tst.ts | 13 +++++++++++++ .../__typetests__/FeatureFlagTest.tsx | 10 ---------- scripts/test-ts.sh | 4 ++++ tstyche.config.json | 6 ++++++ yarn.lock | 15 +++++++++++++++ 6 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 packages/react-native-reanimated/__typetests__/FeatureFlagTest.tst.ts delete mode 100644 packages/react-native-reanimated/__typetests__/FeatureFlagTest.tsx create mode 100644 tstyche.config.json diff --git a/package.json b/package.json index fdb411ab2ffa..c5882a29fa8e 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": "4.1.0", "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/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.config.json b/tstyche.config.json new file mode 100644 index 000000000000..adfd0ca04970 --- /dev/null +++ b/tstyche.config.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://tstyche.org/schemas/config.json", + "testFileMatch": [ + "packages/*/__typetests__/**/*.tst.*" + ] +} diff --git a/yarn.lock b/yarn.lock index b5289d11df55..d64cf6176758 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:4.1.0" typescript: "npm:5.9.3" languageName: unknown linkType: soft @@ -32033,6 +32034,20 @@ __metadata: languageName: node linkType: hard +"tstyche@npm:4.1.0": + version: 4.1.0 + resolution: "tstyche@npm:4.1.0" + peerDependencies: + typescript: ">=4.7" + peerDependenciesMeta: + typescript: + optional: true + bin: + tstyche: ./build/bin.js + checksum: 10/9cc0b8b28a82ece0abc4838df08b64d200839cd832e15cb3654ba07ab38d53d2782f7cc36c94d2a4541b4d3b40a7e57ac58524bdffc990280302dbcbd4b5ac09 + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" From 189c83e08d3ed81b6475783f870764bc199259ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Fri, 29 May 2026 15:04:40 +0200 Subject: [PATCH 2/7] Format tstyche.config.json --- tstyche.config.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tstyche.config.json b/tstyche.config.json index adfd0ca04970..c7cf1352a6e1 100644 --- a/tstyche.config.json +++ b/tstyche.config.json @@ -1,6 +1,4 @@ { "$schema": "https://tstyche.org/schemas/config.json", - "testFileMatch": [ - "packages/*/__typetests__/**/*.tst.*" - ] + "testFileMatch": ["packages/*/__typetests__/**/*.tst.*"] } From beb716a379a129876f66dc821e508522242b47bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Fri, 29 May 2026 17:43:38 +0200 Subject: [PATCH 3/7] Update package.json Co-authored-by: Tom Mrazauskas --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5882a29fa8e..9fc81e7cbaa5 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "remark-gfm": "4.0.1", "remark-mdx": "3.1.0", "shelljs": "0.10.0", - "tstyche": "4.1.0", + "tstyche": "7.2.1", "typescript": "5.9.3" }, "resolutions": { From 9b04888a7995fc70df362aa3c2d67f04196f5c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Fri, 29 May 2026 18:23:12 +0200 Subject: [PATCH 4/7] Allow TSTyche 7.2.1 under the npm age gate The 7.2.1 bump could not be installed: the repo's 8-day npm age gate quarantines it (published 2026-05-27), and the bump left yarn.lock pinned to 4.1.0, which breaks `yarn install --immutable`. - Add tstyche to npmPreapprovedPackages in .yarnrc.yml. - Update yarn.lock to TSTyche 7.2.1. --- .yarnrc.yml | 1 + yarn.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) 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/yarn.lock b/yarn.lock index d64cf6176758..7f4c6033af6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28184,7 +28184,7 @@ __metadata: remark-gfm: "npm:4.0.1" remark-mdx: "npm:3.1.0" shelljs: "npm:0.10.0" - tstyche: "npm:4.1.0" + tstyche: "npm:7.2.1" typescript: "npm:5.9.3" languageName: unknown linkType: soft @@ -32034,17 +32034,17 @@ __metadata: languageName: node linkType: hard -"tstyche@npm:4.1.0": - version: 4.1.0 - resolution: "tstyche@npm:4.1.0" +"tstyche@npm:7.2.1": + version: 7.2.1 + resolution: "tstyche@npm:7.2.1" peerDependencies: - typescript: ">=4.7" + typescript: ">=5.4" peerDependenciesMeta: typescript: optional: true bin: - tstyche: ./build/bin.js - checksum: 10/9cc0b8b28a82ece0abc4838df08b64d200839cd832e15cb3654ba07ab38d53d2782f7cc36c94d2a4541b4d3b40a7e57ac58524bdffc990280302dbcbd4b5ac09 + tstyche: dist/bin.js + checksum: 10/42b21eededd6924b2cd271fb9747be50240b881e5a582d32a2e1bd2e575bd0d50d9a454d82e6ca58b7b6b500be02ac117d579d77f70da6c26dd522ca8f3e97a2 languageName: node linkType: hard From c657e79cc417bf20110edaf844932ffc4fc5a2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Fri, 29 May 2026 18:23:30 +0200 Subject: [PATCH 5/7] Rename TSTyche config to tstyche.json for v7 TSTyche v7 changed the default configuration file name from tstyche.config.json to tstyche.json. Without the rename v7 auto-renames it at runtime and prints a warning. --- tstyche.config.json => tstyche.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tstyche.config.json => tstyche.json (100%) diff --git a/tstyche.config.json b/tstyche.json similarity index 100% rename from tstyche.config.json rename to tstyche.json From 18efd1f5d0e661b959d03beff9b6429f321f7ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Fri, 29 May 2026 18:24:26 +0200 Subject: [PATCH 6/7] Run TSTyche from reanimated type:check:tests CI runs type tests only through each package's `yarn type:check` -> `type:check:tests`, so the standalone `type:check:tstyche` script left the .tst.* tests unenforced now that the old harness skips them. Call `type:check:tstyche`, scoped to the package, from `type:check:tests` so CI keeps checking them. Scoped by package path to avoid cross-package build coupling; worklets stays unwired until it has its first .tst.* test. --- packages/react-native-reanimated/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From e237b4f5abf71e00bcec3b07875fbffc04f04d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Fri, 29 May 2026 19:17:38 +0200 Subject: [PATCH 7/7] Migrate nativeMethods and useAnimatedReaction type tests to TSTyche Convert two more __typetests__ from `@ts-expect-error`/tsc to TSTyche: - nativeMethods: measure/dispatchCommand/scrollTo accept an Animated ref and reject a plain ref (toBeCallableWith / not.toBeCallableWith); plus setGestureState and the no-argument dispatchCommand form. - useAnimatedReaction: prepare/react callability, the optional dependency array, and the previous-result react parameter. Builds on the TSTyche setup and type:check:tests wiring in #9557. --- .../__typetests__/nativeMethodsTest.tst.ts | 66 +++++++++++++++++++ .../__typetests__/nativeMethodsTest.tsx | 44 ------------- .../useAnimatedReactionTest.tst.ts | 36 ++++++++++ .../__typetests__/useAnimatedReactionTest.tsx | 49 -------------- 4 files changed, 102 insertions(+), 93 deletions(-) create mode 100644 packages/react-native-reanimated/__typetests__/nativeMethodsTest.tst.ts delete mode 100644 packages/react-native-reanimated/__typetests__/nativeMethodsTest.tsx create mode 100644 packages/react-native-reanimated/__typetests__/useAnimatedReactionTest.tst.ts delete mode 100644 packages/react-native-reanimated/__typetests__/useAnimatedReactionTest.tsx 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; -}