Register a module on NativeModules and it gets instantiated at app launch. Whether you call it or not.
The calls into it are untyped on top of that, so a renamed native method fails the first time JS actually reaches for it — at runtime, in production if you're unlucky.
#The old way
import { NativeModules } from 'react-native';
const { DeviceInfo } = NativeModules;
async function getBatteryLevel(): Promise<number> {
return DeviceInfo.getBatteryLevel();
}
Nothing checks that DeviceInfo exists. Nothing checks that getBatteryLevel is spelled the same way on the native side. You find out when this line executes.
#The new way
import { TurboModuleRegistry, type TurboModule } from 'react-native';
export interface Spec extends TurboModule {
getBatteryLevel(): Promise<number>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('DeviceInfo');
import DeviceInfo from './NativeDeviceInfo';
async function getBatteryLevel(): Promise<number> {
return DeviceInfo.getBatteryLevel();
}
Codegen reads the Spec interface in NativeDeviceInfo.ts and writes the matching Objective-C++/Java glue at build time. A missing or mis-typed native method breaks the build instead of surprising you at runtime. And the module itself isn't constructed until the first time JS touches TurboModuleRegistry.getEnforcing.
#Why it matters
- Startup cost scales with the modules you actually call, not every module linked into the app.
- Signature drift between JS and native gets caught at build time.
- The typed spec doubles as the contract documentation for the native side.
#Gotcha
Codegen does static analysis, not runtime discovery. It matches on the file's conventional name and on a literal interface Spec extends TurboModule. Rename the spec file away from NativeDeviceInfo.ts, or swap the interface for a type alias, and the module drops out of the codegen output. Silently. No error until you try to call it.