Skip to content

CONCEPT Cited by 1 source

Turbo Module + DI contract

Turbo Module + DI contract is a three-language API contract (TypeScript + Swift + Kotlin) used to let a React Native layer communicate with the surrounding legacy native app while preserving isolation — the bridging discipline in the React Native as a package architecture. Canonicalised by Zalando's 2025-10 post.

The shape

Four pieces (walk-through from the post's wishlist example):

  1. TypeScript turbo-module spec — the RN-side interface.

    export interface Spec extends TurboModule {
      addProduct(
        sku: string,
        shouldShowNotification?: boolean,
      ): Promise<void>;
      readonly onProductChange: EventEmitter<ProductChangeEvent>;
    }
    
  2. Swift protocol — iOS side of the contract.

    @objc
    public protocol WishlistProtocol: AnyObject {
        var onProductChange: ((String, String?, Bool) -> Void)? { get set }
        func addProduct(_ sku: String, shouldShowNotification: Bool, completion: @escaping ((Error?) -> Void))
    }
    
  3. DI injection point — a static delegate slot the legacy app registers its implementation into.

    @objc public class WishlistConfig: NSObject {
        @objc public static var delegate: TurboWishlistProtocol?
    }
    
  4. Kotlin equivalent — same contract mirror for Android.

  5. Legacy app implements and injects. At startup, the legacy app sets WishlistConfig.delegate = MyWishlistImpl(). RN code calls the TypeScript Spec; the RN runtime routes to the Swift/Kotlin protocol; the protocol dispatches to the injected delegate.

The standalone Developer App provides a mocked delegate — so wishlist features can be developed and tested in the Developer App without the legacy app.

Why it's "three-language"

The contract has to exist in TypeScript, Swift, and Kotlin simultaneously, with each declaration honouring the others. This is stricter than a two-language contract (typical RN turbo module) because:

  • TS types must match Swift protocol signatures (including callback shapes, optional parameters).
  • TS types must match Kotlin interface signatures.
  • Swift and Kotlin must converge on the same semantic contract even though they use different dispatch conventions (Swift protocols with @objc, Kotlin interfaces).

Zalando explicitly calls out the process consequence: the API contract must be defined and agreed upon before implementation starts (see patterns/api-contract-first-across-three-languages):

"Especially when combining three environments into one (TypeScript, Swift and Kotlin) it's crucial to first properly define these API contracts and ensure that all involved environments are compatible with this contract as early as possible. Otherwise, you run into challenges where the API design might not be feasible on all platforms, requiring you to undo work that has already been done."

Why DI vs direct coupling

The DI slot (WishlistConfig.delegate: WishlistProtocol?) decouples the Framework SDK from the legacy app:

  • Framework SDK doesn't know which implementation is registered — it just calls through the protocol.
  • Legacy app injects its implementation at startup.
  • Developer App injects a mock implementation.

This is what makes the Developer App viable — without the DI slot, the SDK would hard-depend on the legacy app's wishlist implementation, and the Developer App couldn't link.

Seen in

Last updated · 507 distilled / 1,218 read