skip to content

In a React Native Turbo Module spec, how do TypeScript types such as number, boolean, object literals and Promise map to Kotlin and Objective-C?

level: middleimportance: should knowfreq 30%

answer

  1. every number is a double
  2. nullable turns primitives into boxed types
  3. objects: ReadableMap in, WritableMap out
  4. iOS object params become C++ structs
  5. Promise becomes a trailing native argument

basics

~20 s

Codegen maps number to double, boolean to a native boolean, string to String or NSString, object literals to ReadableMap on Android and a generated struct on iOS, and a Promise return to extra native arguments that settle it.

solid answer

~40 s

Codegen reads the spec and emits native signatures. `number` becomes a `double` on both platforms (Kotlin `Double`), including `Int32` and `Float` aliases in the Android spec, so integers arrive as floating point. `boolean` becomes `Boolean` in Kotlin and `BOOL` in Objective-C; making either nullable boxes it (`Double?`, `NSNumber *`). An object literal parameter arrives as a `ReadableMap` on Android and as a generated C++ struct on iOS; object return values are `WritableMap` and `NSDictionary *`. Arrays map to `ReadableArray` and `NSArray`. A function parameter becomes `Callback` or `RCTResponseSenderBlock`. A `Promise<T>` return disappears from the native return type: the method returns `void` and gains a trailing `Promise` parameter on Android or `resolve:`/`reject:` blocks on iOS.

code

typescript · 12 lines
typescript
import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';

export type StrapConfig = {deviceId: string; sampleRateHz: number};

export interface Spec extends TurboModule {
  configure(config: StrapConfig): void;
  batteryLevel(): number;
  connect(deviceId: string, retry: boolean | null): Promise<boolean>;
}

export default TurboModuleRegistry.getEnforcing<Spec>('NativeHeartRate');

go deeper

for a junior

Recall that JavaScript numbers become doubles natively and that a Promise method gets extra native arguments to settle it instead of a return value.

for a middle

Walk through the parameter and return tables for both platforms, including nullable boxing and how object literals differ between ReadableMap and generated structs.

for a senior

Explain which mismatches Codegen catches and which only fail at runtime, and how you keep Android maps, iOS dictionaries and the TypeScript type in step.

for a principal

Decide how strictly a shared module's contract should be typed, whether to prefer flat primitives over objects for safety, and how to test both platforms against one spec.

## Why the mapping matters A **Turbo Module spec** is written in TypeScript or Flow, but the module is implemented in Kotlin or Java on Android and Objective-C++ on iOS. **Codegen** reads the spec and generates a native base class (Android) and protocol (iOS) whose method signatures are fixed by the spec's types. If the native method does not match, the compiler complains or the call fails at runtime, so knowing the mapping is how you read a Codegen error and write the implementation first time. ## Parameter types | Spec type | Android (generated spec, as Kotlin) | iOS (generated protocol) | |---|---|---| | `string` | `String` | `NSString *` | | `number` | `Double` | `double` | | `boolean` | `Boolean` | `BOOL` | | `number` or `boolean`, nullable | `Double?` / `Boolean?` | `NSNumber *` | | object literal or type alias | `ReadableMap` | a generated C++ struct, passed by reference | | `Array<T>` | `ReadableArray` | `NSArray *` | | function | `Callback` | `RCTResponseSenderBlock` | Three details trip people up: - **All numbers are doubles.** JavaScript has one number type, so `number` is a double on both platforms. On Android the `Int32`, `Float` and `Double` spec aliases also become `double` parameters in the generated Java spec, so a Kotlin override receives `Double` and must convert if it needs an `Int`. - **Nullability changes the native type**, not just an annotation. A required `boolean` is a primitive; `boolean | null` is a boxed value (`NSNumber *` on iOS), and the implementation must check for null. - **Object literals are typed on iOS, untyped on Android.** On iOS Codegen generates a struct with accessor methods for each field. On Android the parameter is a `ReadableMap`, and the Kotlin code reads fields by string key (`getDouble("bpm")`), so a renamed field compiles fine and fails at runtime. ## Return types | Spec return | Android | iOS | |---|---|---| | `void` | `Unit` / `void` | `void` | | `string` | `String` | `NSString *` | | `number` | `Double` | `NSNumber *` | | object | `WritableMap` | `NSDictionary *` | | `Array<T>` | `WritableArray` | `NSArray *` | | `Promise<T>` | `void` plus a trailing `Promise` parameter | `void` plus `resolve:` and `reject:` blocks | Object results are built natively: on Android with `Arguments.createMap()` and its `put...` methods, on iOS as a dictionary literal. Nothing checks at runtime that the keys match the TypeScript type, so the spec is only as honest as the native code that fills the map. ## How a Promise is represented A spec method `connect(deviceId: string): Promise<boolean>` has no native `Promise<Boolean>` return type. Instead: 1. The native method returns `void`. 2. Android appends a `com.facebook.react.bridge.Promise` parameter: `fun connect(deviceId: String, promise: Promise)`. 3. iOS appends `resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject`. 4. Native code settles it with `resolve(value)` or `reject(code, message)`. The `T` in `Promise<T>` documents what JavaScript receives, but native code passes an untyped value to `resolve`, so sending a string where the spec promises a boolean is not caught at compile time. ## Event payloads For a `CodegenTypes.EventEmitter<T>` property, Codegen generates an `emit...` method whose parameter follows the same rules: an object payload is a `ReadableMap` on Android and a dictionary on iOS. For a heart-rate strap you would build `{bpm, timestamp}` by hand on each platform, keeping the keys identical to the TypeScript type. ## Reading a mismatch after a spec change When a spec type changes, the generated signatures change with it. On Android the abstract method in the generated spec class no longer matches your override, so the Kotlin compiler reports a method that overrides nothing and an abstract method left unimplemented. On iOS the generated protocol gains the new selector, and the compiler warns that the class does not implement it. Reading those messages against the tables above usually points straight at the type that moved, such as a `number` that became nullable. ## Practical rules - Prefer **object literal types** over the generic `Object` type; the React Native docs recommend literals because they give Codegen fields to type. - Keep payloads flat and JSON-like: strings, numbers, booleans, arrays and nested object literals. - Convert numbers deliberately on the native side instead of trusting that a `number` holds an integer.

  • Why does a field rename in the TypeScript type not break the Android build?
    On Android an object literal parameter arrives as a `ReadableMap`, and Kotlin reads fields by string key. Codegen checks the method signature, not the keys the implementation reads, so a renamed field compiles and fails only when the key lookup comes back missing at runtime. On iOS the generated struct exposes typed accessors, so a rename does surface at compile time there.
  • How should a module return a structured result from a Promise?
    Declare the shape in the spec as `Promise<SomeObjectType>`, then build a `WritableMap` with `Arguments.createMap()` on Android or an `NSDictionary` on iOS and pass it to `resolve`. The generic `T` is documentation only, so both platforms must fill the same keys by hand.

saying these in an interview costs you the question

  • A spec number with Int32 arrives in Kotlin as an Int
  • A Promise return maps to a native Promise or Future return type
  • Codegen validates object keys the native code puts into maps
  • Nullable spec types keep the same primitive native type
  • Object literal parameters become generated data classes on Android