Sanniv Khare 6 min read

React Native 0.88.1 strict TypeScript: ten deep imports tested

React Native 0.88.1 strict TypeScript: ten deep imports tested

On React Native 0.88.1 with TypeScript 7.0.2, ten typical react-native/Libraries/* deep imports gave 10 type errors under the default config, 5 with the react-native-legacy-deep-imports opt-out, and 0 once rewritten to imports from react-native itself. The takeaway: the opt-out is not a free pass. It brings back the old .d.ts files, which only have named exports, so the default imports most older code uses still fail. Rewrite to root exports now; nine of our ten had a direct equivalent.

We tested this on 6 Nov 2026 in a Linux container (Ubuntu 24.04, Node 22.23.4) against a fresh install of react-native@0.88.1, @react-native/typescript-config@0.88.1, typescript@7.0.2 and Jest 30.5.2.

Why we did this: our Beacon-type banking apps started on React Native 0.76, and native-module wrappers written in that era reach into react-native/Libraries for event emitters, Platform and codegen helpers. The Strict TypeScript API has been the default since 0.87, and the React Native docs say the opt-out will be removed in a future release without naming one. We wanted to know how much of the opt-out actually helps before deciding whether to lean on it.

What changed in the package

The answer is in node_modules/react-native/package.json. In 0.88.1 the ./Libraries/* export has "types": null, so TypeScript finds no types for any deep path. Only the custom condition maps it to something:

"./Libraries/*": {
  "types": null,
  "default": "./Libraries/*.js",
  "react-native-legacy-deep-imports": [
    "./types_DEPRECATED/Libraries/*.d.ts",
    "./Libraries/*.d.ts"
  ]
}

The runtime path is untouched ("default": "./Libraries/*.js"), so the app still bundles and runs. This is purely a type-checking change, which is why it shows up in CI rather than on a device.

The test: ten deep imports, four configs

We wrote one module with ten deep imports of the kinds that turn up in 0.7x-era code: NativeEventEmitter, RCTDeviceEventEmitter, Platform, codegenNativeComponent, TurboModuleRegistry, the TurboModule type, ViewProps, TextInputProps, processColor and resolveAssetSource. Then we ran tsc --noEmit against it with four configs, all extending @react-native/typescript-config.

tsc error counts for ten deep imports under four tsconfig setups on React Native 0.88.1

Error counts from tsc --noEmit, run in the container on 6 Nov 2026.

  • Default config (strict): 10 errors, all TS2307 Cannot find module. Every deep import fails, as expected.
  • Legacy condition, imports as written: 5 errors. The other five pass, including the type-only imports. NativeEventEmitter, RCTDeviceEventEmitter, Platform and processColor fail with TS2351, TS2339 and TS2349, because the deprecated .d.ts files export them by name (export const Platform, export function processColor) while the code imports a default export, which is what the JS files provide at runtime.
  • Legacy condition, switched to named imports: 1 error. react-native/Libraries/Image/resolveAssetSource has a JS file but no .d.ts in either location, so it never had types on this path.
  • Root imports, default config: 0 errors.

The legacy condition in our tsconfig looked like this:

{
  "extends": "@react-native/typescript-config",
  "compilerOptions": {
    "customConditions": ["react-native", "react-native-legacy-deep-imports"]
  }
}

So if your wrappers use default imports from deep paths, turning on the opt-out still leaves you editing every one of those lines. At that point you might as well point them at the root export.

The rewrite

Nine of the ten have a root export in 0.88.1: we checked each name against types_generated/index.d.ts. RCTDeviceEventEmitter becomes DeviceEventEmitter. The tenth, resolveAssetSource, is not exported on its own, but Image.resolveAssetSource covers the same call.

Diff from deep imports to root exports from react-native

The rewritten module type-checks with the default strict config.

One error came up after the rewrite that the deep imports had hidden: our test Spec for a TurboModule was passed to new NativeEventEmitter(...), and the strict types require the module to declare addListener(eventName: string): void and removeListeners(count: number): void. Older native-module wrappers often skip those two methods in the spec. Adding them fixed it and matches what the native side has to implement anyway.

Jest setup: InitializeCore and setup-env

The docs say react-native/Libraries/Core/InitializeCore is deprecated since 0.87 and should be replaced with react-native/setup-env. We checked both:

# jest.config.js: preset '@react-native/jest-preset', setupFiles: ['<rootDir>/tests/setup-old.js']
npx jest -c jest.old.config.js   # require('react-native/Libraries/Core/InitializeCore')
npx jest -c jest.new.config.js   # require('react-native/setup-env')

Both passed the same smoke test (timers, requestAnimationFrame, Platform.OS, NativeEventEmitter) in Jest 30.5.2. The difference only shows in a TypeScript setup file: import 'react-native/Libraries/Core/InitializeCore' fails with TS2882 under the default config, and import 'react-native/setup-env' passes. The 0.88.1 Jest preset already maps ^react-native/setup-env$ to src/setup-env.js, so no extra moduleNameMapper is needed.

What about third-party libraries?

We installed nine libraries that a banking app of ours typically carries and grepped their shipped code for react-native/Libraries/:

Library Version Files with deep imports
react-native-safe-area-context 5.11.1 10 (codegen specs)
react-native-gesture-handler 3.5.0 8
react-native-screens 4.30.1 3
react-native-keychain, react-native-biometrics, async-storage, device-info, react-native-svg, @react-navigation/native latest 0

None of them broke tsc for the app: a test file importing seven of them (safe-area-context, screens, gesture-handler, keychain, async-storage, device-info, React Navigation) gave 0 errors, because their types resolve to .d.ts files under lib/typescript and skipLibCheck is on in the React Native preset. With --skipLibCheck false the same file gave 9 errors, one of them an unresolved deep import inside safe-area-context’s NativeSafeAreaView.d.ts. Library deep imports are the maintainers’ problem, not a blocker for your upgrade, as long as you keep skipLibCheck on.

Finding your own count

This one-liner lists every deep import in app code, tests and patches. Run it from the project root:

grep -rnoE "(from|require\()\s*['\"]react-native/Libraries/[^'\"]+" \
  src tests patches jest.setup.* 2>/dev/null \
  | sed -E "s/.*react-native\/Libraries\///" | sort | uniq -c | sort -rn

On our test tree it found 21 lines: two copies of the ten imports plus the old Jest setup import.

Our decision

We will not rely on react-native-legacy-deep-imports. In our test it fixed half the errors, the docs say it is going away, and the remaining fixes are the same edits as a full rewrite. For the banking apps the plan is the grep above, a rewrite to root exports, the two missing methods in each TurboModule spec, and react-native/setup-env in Jest setup, done before the 0.89 upgrade rather than during it.

Need this built? Let’s talk.

We build and run apps for Mac, TV, mobile and the web, and write about what we learn.