What rules must a React Native Turbo Native Module spec file such as NativeBatteryHealth.ts follow for Codegen to accept it?
answer
- file name decides discovery
- Native prefix, not a .d.ts
- one interface, named Spec
- one typed registry call
- string literal, letters, digits, underscore
basics
~10 sName the file Native<Something>.ts, declare exactly one interface extending TurboModule named Spec, and load it with exactly one typed TurboModuleRegistry.getEnforcing<Spec>() or get<Spec>() call whose argument is a string literal of letters, digits and underscores.
solid answer
~40 sCodegen is strict because it parses the file statically. The **file name** must start with `Native` (component specs instead end in `NativeComponent`); files in test folders and `.d.ts` files are skipped, and a platform suffix such as `NativeBatteryHealth.android.ts` makes a platform-only spec. Inside, there must be **exactly one** interface extending `TurboModule`, and it must be **named `Spec`**; its members may be methods or non-nullable `EventEmitter` properties, nothing else. The file must contain **exactly one** module load, typed as `TurboModuleRegistry.getEnforcing<Spec>(...)` or `get<Spec>(...)`, with a **single string-literal** argument matching `^[a-zA-Z_][a-zA-Z0-9_]*$`. Breaking a rule stops the build with a parser error prefixed `Module <name>:`; a wrong file name is worse, because Codegen silently skips the file and the build fails later on a missing generated class.
code
typescript · 12 lines// specs/BatteryHealthSpec.ts -- skipped: file name does not start with Native
import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';
const MODULE = 'NativeBatteryHealth';
export interface BatteryHealth extends TurboModule { // must be named Spec
level: number; // properties are rejected
getBatteryLevel(): number;
}
export default TurboModuleRegistry.getEnforcing<BatteryHealth>(MODULE); // needs a string literalgo deeper
Remember the naming convention: module spec files start with Native, and the interface inside is called Spec.
Walk through each rule: file name, a single Spec interface of methods, one typed registry call with a literal name, and the parser error each break produces.
Recognise the silent failure: a misnamed or misplaced spec yields no Codegen error, only a later missing-class build error, and check jsSrcsDir and the file name first.
Put spec conventions into lint or review checklists for every native module team, since the cheapest defects to fix are the ones Codegen would otherwise skip silently.
## Why the rules exist **Codegen** does not execute your spec. It finds candidate files by name, parses them into a schema, and generates native code from that schema. Everything it needs must therefore be visible statically: the interface, the types and the module name. The rules below are what the parser enforces in React Native 0.87, and each has a recognisable failure. ## Rule 1: the file name - A module spec's file name must **start with `Native`**: `NativeBatteryHealth.ts` qualifies, `BatteryHealthSpec.ts` does not. (Fabric component specs follow a different rule: the name ends in `NativeComponent`.) - Files under test folders and TypeScript declaration files (`.d.ts`) are ignored. - A **platform suffix** is allowed: `NativeBatteryHealth.android.ts` is used only when generating for Android. - The part of the name before the first dot becomes the generated module's name, which is why the Android class is `NativeBatteryHealthSpec`. The failure mode here is the nastiest: a misnamed file raises no error. Codegen skips it, no interface is generated, and the build fails later when native code cannot find the class it extends. ## Rule 2: exactly one interface, named `Spec` - The file must declare **one** interface extending `TurboModule`. Zero gives "No TypeScript interfaces extending TurboModule were detected in this NativeModule spec"; two or more gives "Every NativeModule spec file must declare exactly one NativeModule TypeScript interface". - It must be named **`Spec`**: otherwise "All TypeScript interfaces extending TurboModule must be called 'Spec'". - Its members must be **methods** or non-nullable **`EventEmitter`** properties. A plain data property such as `level: number` is rejected. ## Rule 3: exactly one typed module load 1. The file must contain **exactly one** call to `TurboModuleRegistry.getEnforcing` or `TurboModuleRegistry.get`. 2. The call must be **typed** with the spec: `getEnforcing<Spec>('NativeBatteryHealth')`. An untyped call gets "Please type this NativeModule load". 3. It takes **exactly one argument**, and that argument must be a **string literal**, not a variable or template. 4. The name must be **safe**: letters, digits and underscores, not starting with a digit. 5. A spec interface that is never loaded is an error too: "Unused NativeModule spec". ## Rule 4: supported types only Parameter and return types must come from the set Codegen can translate (React Native's appendix lists them). An unsupported annotation, a generic Codegen does not recognise, or a generic with the wrong number of type parameters is a parser error naming the offending type. ## Reading the errors | Symptom | Likely rule broken | |---|---| | Build fails: native code cannot find `NativeBatteryHealthSpec` | File not named `Native...`, or outside `jsSrcsDir` | | `must be called 'Spec'` | Interface misnamed | | `exactly one NativeModule load` | Two registry calls in one file | | `with a string literal` | Name passed through a constant | | `Please type this NativeModule load` | Missing `<Spec>` type argument | Parser errors are prefixed with `Module <name>:`, so the module that broke is named for you. Codegen collects the errors in a file but currently throws only the **first** one, so a spec with several problems reveals them one fix at a time. ## A checklist before committing a spec - The file is named `Native<Something>.ts` and sits under `jsSrcsDir`. - It declares one `Spec` interface extending `TurboModule`, containing only methods (and, where needed, `EventEmitter` properties). - It has one typed `TurboModuleRegistry` call with a literal, safe name. - Running Codegen locally produces the expected class, with no `Module <name>:` error. - The native module registers under exactly the same name. ## A note on the module name The string passed to the registry is the name the **native module registers under**. The docs' own example uses the same string for the file and the native module (`NativeLocalStorage`), which is the least surprising convention. If the Android `getName()` or the iOS registration returns a different string, the spec still compiles, but the lookup fails at runtime. ## Summary Name the file `Native...`, declare one interface called `Spec` that extends `TurboModule`, and load it once with a typed registry call and a literal, safe module name. Most rule breaks produce a clear `Module <name>:` parser error; a misnamed file produces silence and a missing class, which is the one to recognise on sight.
- Why can't the module name be a shared constant imported from another file?Codegen reads the spec statically and records the name from the call's argument. It only accepts a string literal, and the parser reports an error asking for one when it sees a variable. A constant would require evaluating code, which the generator deliberately does not do.
- How do you give Android and iOS slightly different specs?Use platform-suffixed spec files, such as `NativeBatteryHealth.android.ts` and `NativeBatteryHealth.ios.ts`. Codegen accepts platform-agnostic specs for every platform and platform-suffixed specs only when generating for that platform. Keep the shared surface identical so app code stays platform-neutral.
saying these in an interview costs you the question
- Codegen finds specs by scanning for any interface extending TurboModule
- A misnamed spec file fails with a clear Codegen error
- The spec interface can have any name if it extends TurboModule
- One spec file can declare several modules for convenience
- The module name may come from an imported constant