From c8cf6c72a4450d3f946f174c1f40c5a894487b4a Mon Sep 17 00:00:00 2001 From: Sam Zhou Date: Wed, 14 Oct 2026 10:28:25 -0700 Subject: [PATCH] Add `LegacyObject` to keep Flow `Object` as `Object` in the TypeScript API Summary: Flow will soon give `Object` the same meaning as TypeScript's `Object`, rather than treating it as `any`. React Native sources that use `Object` as an untyped escape hatch need to move off it. But the generated TypeScript API (`ReactNativeApi.d.ts`) relies on Flow `Object` mapping to TS `Object`, and it should keep emitting `Object`. This adds a `LegacyObject` global type to bridge the two: - Flow: `flow/LegacyObject.js` declares `LegacyObject` as `any`, next to `Stringish`. - TypeScript: build-types replaces `LegacyObject` with `Object` before translation, via a new `replaceLegacyObjectWithObject` pre-transform that mirrors `replaceStringishWithString`. - Codegen: native module specs accept `LegacyObject` as a generic object, like `Object` and `UnsafeObject`. `CodegenTypes.UnsafeObject`, the event object types in `EventEmitter` and `NativeEventEmitter`, `PushNotificationIOS`, and the native module specs that used bare `Object` now use `LegacyObject`. Neither the generated TypeScript API nor the codegen schema changes. The codegen parser's internal AST aliases (`PropAST`, `ASTNode`) switch to plain `any`. Changelog: [Internal] Differential Revision: D125245815 --- .../__tests__/parsers-primitives-test.js | 20 ++++++++ .../src/parsers/parsers-primitives.js | 1 + .../react-native-codegen/src/parsers/utils.js | 4 +- .../EventEmitter/NativeEventEmitter.js | 3 +- .../PushNotificationIOS.js | 21 +++----- .../Libraries/Types/CodegenTypes.js | 3 +- .../Libraries/vendor/emitter/EventEmitter.js | 6 +-- packages/react-native/flow/LegacyObject.js | 13 +++++ .../modules/NativeExceptionsManager.js | 3 +- .../modules/NativeSegmentFetcher.js | 8 +-- .../replaceLegacyObjectWithObject-test.js | 49 +++++++++++++++++++ .../flow/replaceLegacyObjectWithObject.js | 43 ++++++++++++++++ .../js-api/build-types/translateSourceFile.js | 1 + 13 files changed, 145 insertions(+), 30 deletions(-) create mode 100644 packages/react-native/flow/LegacyObject.js create mode 100644 scripts/js-api/build-types/transforms/flow/__tests__/replaceLegacyObjectWithObject-test.js create mode 100644 scripts/js-api/build-types/transforms/flow/replaceLegacyObjectWithObject.js diff --git a/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js b/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js index 6e198275c457..3b9f61dc2383 100644 --- a/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js +++ b/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js @@ -1902,6 +1902,26 @@ describe('emitUnion', () => { }); }); + describe("when 'typeAnnotation.id.name' is 'LegacyObject'", () => { + const typeAnnotation = { + typeParameters: { + params: [1, 2], + type: 'GenericObjectTypeAnnotation', + }, + id: { + name: 'LegacyObject', + }, + }; + const expected = { + type: 'GenericObjectTypeAnnotation', + }; + const result = emitCommonTypesForUnitTest(typeAnnotation, false); + + it("returns 'GenericObjectTypeAnnotation'", () => { + expect(result).toEqual(expected); + }); + }); + describe("when 'typeAnnotation.id.name' is 'Object'", () => { const typeAnnotation = { typeParameters: { diff --git a/packages/react-native-codegen/src/parsers/parsers-primitives.js b/packages/react-native-codegen/src/parsers/parsers-primitives.js index b9e62496435d..7c6de3bbe7ba 100644 --- a/packages/react-native-codegen/src/parsers/parsers-primitives.js +++ b/packages/react-native-codegen/src/parsers/parsers-primitives.js @@ -667,6 +667,7 @@ function emitCommonTypes( Double: emitDouble, Float: emitFloat, UnsafeObject: emitGenericObject, + LegacyObject: emitGenericObject, Object: emitGenericObject, $Partial: emitPartial, Partial: emitPartial, diff --git a/packages/react-native-codegen/src/parsers/utils.js b/packages/react-native-codegen/src/parsers/utils.js index 207f357bea2a..e6bc2462bc05 100644 --- a/packages/react-native-codegen/src/parsers/utils.js +++ b/packages/react-native-codegen/src/parsers/utils.js @@ -34,10 +34,10 @@ function extractNativeModuleName(filename: string): string { export type ParserErrorCapturer = (fn: () => T) => ?T; // $FlowFixMe[unclear-type] there's no flowtype for ASTs -export type PropAST = Object; +export type PropAST = any; // $FlowFixMe[unclear-type] there's no flowtype for ASTs -export type ASTNode = Object; +export type ASTNode = any; function createParserErrorCapturer(): [ Array, diff --git a/packages/react-native/Libraries/EventEmitter/NativeEventEmitter.js b/packages/react-native/Libraries/EventEmitter/NativeEventEmitter.js index ed018ab6ad63..4d02c952e431 100644 --- a/packages/react-native/Libraries/EventEmitter/NativeEventEmitter.js +++ b/packages/react-native/Libraries/EventEmitter/NativeEventEmitter.js @@ -45,8 +45,7 @@ export type {EventSubscription, EmitterSubscription}; /** @deprecated Use `EventSubscription` instead. */ export type NativeEventSubscription = EventSubscription; -// $FlowFixMe[unclear-type] unclear type of events -type UnsafeNativeEventObject = Object; +type UnsafeNativeEventObject = LegacyObject; /** * `NativeEventEmitter` is intended for use by Native Modules to emit events to diff --git a/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js b/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js index 7de529ce4f8b..956100a7d777 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js +++ b/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js @@ -123,8 +123,7 @@ export interface PushNotification { /** * An alias for `getAlert` to get the notification's main message string */ - // $FlowFixMe[unclear-type] - getMessage(): ?string | ?Object; + getMessage(): ?string | ?LegacyObject; /** * Gets the sound string from the `aps` object @@ -139,8 +138,7 @@ export interface PushNotification { /** * Gets the notification's main message from the `aps` object */ - // $FlowFixMe[unclear-type] - getAlert(): ?string | ?Object; + getAlert(): ?string | ?LegacyObject; /** * Gets the content-available number from the `aps` object @@ -155,8 +153,7 @@ export interface PushNotification { /** * Gets the data object on the notif */ - // $FlowFixMe[unclear-type] - getData(): ?Object; + getData(): ?LegacyObject; /** * Gets the thread ID on the notif @@ -550,8 +547,7 @@ class PushNotificationIOS { * `getInitialNotification` is sufficient. * */ - // $FlowFixMe[unclear-type] - constructor(nativeNotif: Object) { + constructor(nativeNotif: LegacyObject) { this._data = {}; this._remoteNotificationCompleteCallbackCalled = false; this._isRemote = nativeNotif.remote; @@ -615,8 +611,7 @@ class PushNotificationIOS { /** * An alias for `getAlert` to get the notification's main message string. */ - // $FlowFixMe[unclear-type] - getMessage(): ?string | ?Object { + getMessage(): ?string | ?LegacyObject { // alias because "alert" is an ambiguous name return this._alert; } @@ -646,8 +641,7 @@ class PushNotificationIOS { * * See https://reactnative.dev/docs/pushnotificationios#getalert */ - // $FlowFixMe[unclear-type] - getAlert(): ?string | ?Object { + getAlert(): ?string | ?LegacyObject { return this._alert; } @@ -674,8 +668,7 @@ class PushNotificationIOS { * * See https://reactnative.dev/docs/pushnotificationios#getdata */ - // $FlowFixMe[unclear-type] - getData(): ?Object { + getData(): ?LegacyObject { return this._data; } diff --git a/packages/react-native/Libraries/Types/CodegenTypes.js b/packages/react-native/Libraries/Types/CodegenTypes.js index b700b228197e..cf9c22b2f429 100644 --- a/packages/react-native/Libraries/Types/CodegenTypes.js +++ b/packages/react-native/Libraries/Types/CodegenTypes.js @@ -29,8 +29,7 @@ export type DirectEventHandler< export type Double = number; export type Float = number; export type Int32 = number; -/* $FlowFixMe[unclear-type] Object is forbidden in strict mode */ -export type UnsafeObject = Object; +export type UnsafeObject = LegacyObject; export type UnsafeMixed = unknown; type DefaultTypes = number | boolean | string | ReadonlyArray; diff --git a/packages/react-native/Libraries/vendor/emitter/EventEmitter.js b/packages/react-native/Libraries/vendor/emitter/EventEmitter.js index 305965630f24..79df1a4f2c22 100644 --- a/packages/react-native/Libraries/vendor/emitter/EventEmitter.js +++ b/packages/react-native/Libraries/vendor/emitter/EventEmitter.js @@ -8,8 +8,7 @@ * @format */ -// $FlowFixMe[unclear-type] unclear type of events -type UnsafeEventObject = Object; +type UnsafeEventObject = LegacyObject; export interface EventSubscription { remove(): void; @@ -78,8 +77,7 @@ export default class EventEmitter< TEventToArgsMap extends Readonly< Record>, > = Readonly>>, -> implements IEventEmitter -{ +> implements IEventEmitter { #registry: Registry; constructor() { diff --git a/packages/react-native/flow/LegacyObject.js b/packages/react-native/flow/LegacyObject.js new file mode 100644 index 000000000000..2a01abb620ed --- /dev/null +++ b/packages/react-native/flow/LegacyObject.js @@ -0,0 +1,13 @@ +/** + * 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. + * + * @flow strict + * @format + */ + +// The legacy Flow `Object` type, which is `any`. The generated TypeScript API +// (scripts/js-api/build-types) translates this type to `Object`. +declare type LegacyObject = any; diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeExceptionsManager.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeExceptionsManager.js index 28b22c11de98..41aa8cb56e83 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeExceptionsManager.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeExceptionsManager.js @@ -29,8 +29,7 @@ export type ExceptionData = { stack: Array, id: number, isFatal: boolean, - // flowlint-next-line unclear-type:off - extraData?: Object, + extraData?: LegacyObject, ... }; export interface Spec extends TurboModule { diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSegmentFetcher.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSegmentFetcher.js index bf7bfdaa7303..bf318c3b903d 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSegmentFetcher.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSegmentFetcher.js @@ -15,13 +15,13 @@ import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboMod export interface Spec extends TurboModule { readonly fetchSegment: ( segmentId: number, - options: Object, // flowlint-line unclear-type: off - callback: (error: ?Object) => void, // flowlint-line unclear-type: off + options: LegacyObject, + callback: (error: ?LegacyObject) => void, ) => void; readonly getSegment?: ( segmentId: number, - options: Object, // flowlint-line unclear-type: off - callback: (error: ?Object, path: ?string) => void, // flowlint-line unclear-type: off + options: LegacyObject, + callback: (error: ?LegacyObject, path: ?string) => void, ) => void; } diff --git a/scripts/js-api/build-types/transforms/flow/__tests__/replaceLegacyObjectWithObject-test.js b/scripts/js-api/build-types/transforms/flow/__tests__/replaceLegacyObjectWithObject-test.js new file mode 100644 index 000000000000..b14e228bb1b1 --- /dev/null +++ b/scripts/js-api/build-types/transforms/flow/__tests__/replaceLegacyObjectWithObject-test.js @@ -0,0 +1,49 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +const replaceLegacyObjectWithObject = require('../replaceLegacyObjectWithObject'); +const {parse, print} = require('flow-transform'); + +const prettierOptions = {parser: 'babel'}; + +async function translate(code: string): Promise { + const parsed = await parse(code); + const result = await replaceLegacyObjectWithObject(parsed); + return print(result.ast, result.mutatedCode, prettierOptions); +} + +describe('replaceLegacyObjectWithObject', () => { + test('should replace LegacyObject with Object in return annotation', async () => { + const code = `function foo(): LegacyObject {}`; + const result = await translate(code); + expect(result).toMatchInlineSnapshot(` + "function foo(): Object {} + " + `); + }); + + test('should replace LegacyObject with Object in nullable union', async () => { + const code = `let foo: ?string | ?LegacyObject;`; + const result = await translate(code); + expect(result).toMatchInlineSnapshot(` + "let foo: ?string | ?Object; + " + `); + }); + + test('should replace LegacyObject with Object in type alias', async () => { + const code = `type Foo = LegacyObject;`; + const result = await translate(code); + expect(result).toMatchInlineSnapshot(` + "type Foo = Object; + " + `); + }); +}); diff --git a/scripts/js-api/build-types/transforms/flow/replaceLegacyObjectWithObject.js b/scripts/js-api/build-types/transforms/flow/replaceLegacyObjectWithObject.js new file mode 100644 index 000000000000..6775112d5714 --- /dev/null +++ b/scripts/js-api/build-types/transforms/flow/replaceLegacyObjectWithObject.js @@ -0,0 +1,43 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import type {TransformVisitor} from 'flow-transform'; +import type {ParseResult} from 'flow-transform/dist/transform/parse'; +import type {TransformASTResult} from 'flow-transform/dist/transform/transformAST'; + +const {transformAST} = require('flow-transform/dist/transform/transformAST'); + +const visitors: TransformVisitor = context => ({ + GenericTypeAnnotation(node): void { + if (node.id.name === 'LegacyObject') { + // $FlowExpectedError[incompatible-type] - replaceNode doesn't accept a GenericTypeAnnotation object literal + context.replaceNode(node, { + type: 'GenericTypeAnnotation', + id: { + type: 'Identifier', + name: 'Object', + optional: false, + }, + }); + } + }, +}); + +/** + * Replaces all LegacyObject type references with Object. LegacyObject is + * `any` in Flow, but should keep the `Object` type in the TypeScript API. + */ +async function replaceLegacyObjectWithObject( + source: ParseResult, +): Promise { + return transformAST(source, visitors); +} + +module.exports = replaceLegacyObjectWithObject; diff --git a/scripts/js-api/build-types/translateSourceFile.js b/scripts/js-api/build-types/translateSourceFile.js index 842d89bc7430..0e76ca2004d5 100644 --- a/scripts/js-api/build-types/translateSourceFile.js +++ b/scripts/js-api/build-types/translateSourceFile.js @@ -24,6 +24,7 @@ const preTransforms: Array = [ require('./transforms/flow/replaceRequiresWithImports'), require('./transforms/flow/replaceEmptyWithNever'), require('./transforms/flow/replaceStringishWithString'), + require('./transforms/flow/replaceLegacyObjectWithObject'), require('./transforms/flow/replaceNullablePropertiesWithUndefined'), require('./transforms/flow/reattachDocComments'), require('./transforms/flow/ensureNoUnprefixedProps'),