OpenAIExpoJun 2, 2026, 3:00 PM

Native code in Expo SDK 56: inline modules and type generation

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

openaienmodel: gpt-5-mini-2025-08-07

Native code in Expo SDK 56: inline modules and type generation

Key Points

  • Write inline Swift/Kotlin modules next to app code
  • Auto-generate TypeScript interfaces with expo-type-information CLI
  • Typegen limited to Swift on macOS; filenames must be unique

Summary

Expo SDK 56 introduces inline modules and the expo-type-information package to reduce boilerplate when adding native code to a managed Expo project. You can now drop Swift or Kotlin module files directly into watched project folders, run a one-time prebuild step, and generate TypeScript interfaces automatically with a CLI. This speeds development of native modules and custom native views while keeping generated types in sync with native declarations.

Key Points

  • Enable inline modules by adding one or more directories to your app config: set experiments.inlineModules.watchedDirectories to a list of folders and run npx expo prebuild.
  • Write native files next to your JS/TS code (e.g., app/MyView.swift or app/MyView.kt). Access modules from JS with requireNativeModule('ModuleName') or requireNativeView('ModuleName').
  • Use the expo-type-information CLI (inline-modules-interface) to generate TypeScript: it produces [Module].generated.ts (auto-overwritten) and [Module].tsx (stable, editable) next to the native file.
  • Prebuild updates Xcode and Android project settings, creates mirror/symlinked directories for Android, and enables autolinking so inline modules are included at build time.
  • Limitations: type generation currently only supports Swift modules and runs on macOS; inline module filenames must match module names and be globally unique.
  • Common type-resolution issues: unresolved or imported types and deeply nested DSL closures can yield unknown in generated types. Fixes: explicitly annotate closure return types or add return, or try the CLI option --type-inference PREPROCESS_AND_INFERENCE to improve inference.

Practical checklist for engineers

  • Add watched directory in app config.
  • Run npx expo prebuild to sync native projects.
  • Add .swift / .kt files under the watched folder and implement the Expo module DSL.
  • Run expo-type-information CLI (inline-modules-interface) to generate TypeScript artifacts.
  • Edit the stable [Module].tsx if you need manual adjustments; avoid editing the .generated.ts file.
  • If types are unknown, annotate Swift DSL closures or try --type-inference.

Full Translation

Translations

A translation section that keeps the flow of the original article.

openaijamodel: gpt-5-mini-2025-08-07

Expo SDK 56 のネイティブコード:インラインモジュールと型生成

概要

ネイティブコードを React Native アプリに統合するには手間がかかります。Expo モジュールはその手助けをしますが、利用時に主に次の 2 点が障壁になっていました:

  • パッケージのボイラープレート: モジュールを standalone なパッケージとして作成し、そのセットアップが完了してからネイティブコードを書き始める必要がある。
  • 複数のインターフェース: Swift/Kotlin 側の定義と一致する TypeScript モジュールインターフェースを手動で維持する必要がある。

SDK 56 ではこれらの課題に取り組み、inline-modules と expo-type-information パッケージを導入しました。これにより新しいモジュールの作成がより簡単かつ高速になります。

インラインモジュールと型生成の利用

インラインモジュールの主な考え方はオーバーヘッドを最小化することです。Swift と Kotlin のモジュールをプロジェクト構成内(他のアプリファイルのそば)に直接書けるようになりました。カスタムのネイティブビューが必要な場合は、App.tsx の隣に NativeView.kt や NativeView.swift を作成してビューを実装できます。

インラインモジュールの設定

インラインモジュールの設定は非常に簡単です: アプリの設定ファイルで watchedDirectories を指定します — これはインラインモジュールを含むプロジェクト内のディレクトリのリストです。次に npx expo prebuild を実行してネイティブプロジェクトを同期すれば準備完了です。

{ "expo": { "experiments": { "inlineModules": { "watchedDirectories": ["app"] } } } }

watchedDirectories に "app" を追加すると、app ディレクトリもしくはそのサブディレクトリ(例: app/nested/ や app/nested/directory/)の任意の場所に Swift と Kotlin ファイルを作成できます。例えば app/nested/InlineModule.swift を開いてモジュールを記述できます。

internal import ExpoModulesCore
class InlineModule: Module {
    public func definition() -> ModuleDefinition {
        Constant("Hello") { return "Hello iOS inline modules!" }
    }
}

作成後は JavaScript 側で requireNativeModule('InlineModule') でアクセスできます。モジュール内にビューがあれば requireNativeView('InlineModule') でインポートできます。

型生成の追加

インラインモジュールはモジュール作成の大部分のボイラープレートを取り除きますが、型生成を組み合わせることで利用インターフェースがさらに簡単になります。ネイティブモジュールを書いたら、TypeScript インターフェースがあると型チェックやオートコンプリートが使えて便利です。

この部分は expo-type-information パッケージが担当し、Swift モジュールを解析して対応する TypeScript 型を自動生成します。パッケージには強力な CLI ツールが付属しています。

CLI ツール

CLI にはインラインモジュール向けのコマンド inline-modules-interface が含まれています。このコマンドはプロジェクト内のすべての Swift インラインモジュールを検出し、各モジュールに対して 2 つの TypeScript ファイルを生成します。コマンド実行後、Swift ファイルの隣に一対のファイルが生成されます: InlineModule.generated.ts と InlineModule.tsx。

生成されるファイル

  • 生成ファイル([ModuleName].generated.ts): モジュールに関するすべての型情報を含みます(関数、定数、クラス、ビューなどの DSL 宣言や、enum / record struct の変換可能な Swift 構造)。このファイルはコマンド実行時に上書きされます。

    /Automatically generated by expo-type-information./ import { ViewProps } from 'react-native'; import { NativeModule } from 'expo'; export declare class InlineModuleNativeModuleType extends NativeModule { readonly Hello: string; }

  • 安定ファイル([ModuleName].tsx): モジュールインターフェースを再エクスポートし、メインビューが存在する場合はそのデフォルトエクスポートを提供します。このファイルは編集可能で、一度変更すれば上書きされません。

    // File hash: 8dfc86f5416afbe08cc1ee581c850fc9cec446479211d85501d9a5e2d24cc534 import { InlineModuleNativeModuleType } from './InlineModule.generated'; import { requireNativeModule, requireNativeView } from 'expo'; const InlineModule: InlineModuleNativeModuleType = requireNativeModule<InlineModuleNativeModuleType>('InlineModule'); export const Hello: string = InlineModule.Hello;

TypeScript モジュールインターフェースをこのように分割することで、自動生成された不完全な出力を手動で調整しつつ、ネイティブ側を更新した際にコアの宣言マッピングを再生成できるようになります。

制限事項

  • ファイル名: インラインモジュールの名前は、定義されているファイル名と完全に一致していなければなりません。さらに、モジュール名はグローバルに一意である必要があります(名前はグローバルオブジェクトからモジュールを取得する識別子として使われるため)。そのため、同一プロジェクト内に app/InlineView.swift と src/InlineView.swift の両方を置くことはできません。
  • 言語とプラットフォームのサポート: 型生成は現在 Swift モジュールかつ macOS 上でのみ動作します。

未解決の型

時としてネイティブモジュール宣言の型を解決できない場合があります。これは主に SourceKitten の制限と、提供されたファイルのみを解析しフルコンパイルプロセスを行わない設計によるものです。Swift 型が解決できない場合、TypeScript では unknown 型として生成されます。よくあるケース:

  • ネストした宣言(例: DSL 内の Class): SourceKitten は深くネストしたクロージャの解析に制限があり、Class 内の宣言の型を見つけにくくなります。そのためクラスメソッドの戻り型が正しく解決されないことがあります(引数は正しく解決される場合が多い)。例えば ExpoBlob モジュール:

    import Foundation import ExpoModulesCore public class ExpoBlob: Module { public func definition() -> ModuleDefinition { Name("ExpoBlob") Class(Blob.self) { Constructor { (blobParts: [EitherOfThree<String, Blob, TypedArray>]?, options: BlobOptions?) in let endings = options?.endings ?? .transparent let blobPartsProcessed = processBlobParts(blobParts, endings: endings) return Blob(blobParts: blobPartsProcessed, options: options ?? BlobOptions()) } Property("size") { (blob: Blob) in blob.size } Property("type") { (blob: Blob) in blob.type } Function("slice") { (blob: Blob, start: Int?, end: Int?, contentType: String?) in let blobSize = blob.size let safeStart = start ?? 0 let safeEnd = end ?? blobSize let relativeStart = safeStart < 0 ? max(blobSize + safeStart, 0) : min(safeStart, blobSize) let relativeEnd = safeEnd < 0 ? max(blobSize + safeEnd, 0) : min(safeEnd, blobSize) return blob.slice(start: relativeStart, end: relativeEnd, contentType: contentType ?? "") } AsyncFunction("text") { (blob: Blob) async -> String in await blob.text() } AsyncFunction("bytes") { (blob: Blob) async -> Data in let bytes = await blob.bytes() return Data(bytes) } } } }

生成結果の一部例:

/*Automatically generated by expo-type-information.*/
import { ViewProps } from 'react-native';
import { NativeModule } from 'expo';
// These types haven't been defined in provided file(s).
export type Data = unknown;
export type TypedArray = unknown;
export type BlobOptions = { type: string; endings: EndingType; };
export enum EndingType { transparent = 'transparent', native = 'native' }
export enum BlobPart { string = 'string', blob = 'blob', data = 'data' }
export declare class Blob {
    slice(
        blob: Blob,
        start: number | undefined,
        end: number | undefined,
        contentType: string | undefined
    ): unknown /*The type couldn't be resolved automatically.*/;
    text(blob: Blob): Promise<string>;
    bytes(blob: Blob): Promise<Data>;
    readonly size: unknown /*The type couldn't be resolved automatically.*/;
    readonly type: unknown /*The type couldn't be resolved automatically.*/;
    constructor(
        blobParts: (string | Blob | TypedArray)[] | undefined,
        options: BlobOptions | undefined
    );
}
export declare class ExpoBlobNativeModuleType extends NativeModule { Blob: typeof Blob; }

この例では slice の戻り型や type / size が解決されていません。多くの場合は Swift 側でクロージャの戻り型を明示的に注釈することで修正できます。

  • インポートされた宣言: 我々は提供されたファイルのみを解析し、import を無視するため、外部からの関数や型が戻り値に影響する場合に解決できないことがあります。
  • 戻り値の型: return キーワードを省略し、クロージャへ明示的な注釈を付けていないと戻り型の推論が難しくなります。これを避けるには DSL 宣言に注釈を付けるか、明示的に return を入れてください。

型推論を改善するために、CLI の --type-inference PREPROCESS_AND_INFERENCE オプションを試すことができます。デフォルトでは無効ですが、稀に失敗するケースがあるためです。モジュールによっては有効化で改善することがあります。

Deep dive(仕組みの詳細)

以下ではインラインモジュールと expo-type-information が内部でどのように動作するかをもう少し詳しく説明します。

インラインモジュールの挙動

app/nested/InlineModule.swift にモジュールを作成したとします。まずは app 設定で watchedDirectories を設定します(ここでは app フォルダのみを例にします)。

{ "expo": { "experiments": { "inlineModules": { "watchedDirectories": ["app"] } } } }

Prebuild

アプリ設定更新後、ネイティブプロジェクトを更新するために npx expo prebuild を実行します。これにより主に 2 つの処理が行われます:

  • Xcode プロジェクトを更新して app フォルダを file system synchronized group にします。これによりフォルダ内(およびサブフォルダ内)のすべてのファイルが Xcode エディタに表示され、iOS ビルドに自動的に含まれるようになります。

    • file system synchronized group
  • iOS と Android のプロジェクトプロパティを watchedDirectories で更新します。Android 側ではこれによりファイルが Android Studio に追加されます。これらのプロパティは後の autolinking ステップでも使用されます。

    { "expo.jsEngine": "hermes", "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", "expo.inlineModules.watchedDirectories": "["app"]" }

    ...

    expo.inlineModules.watchedDirectories=["app"]

Android プロジェクト

Android 側では Gradle の設定フェーズ中にプロジェクトが更新されます。これは Android Studio の "Sync Project with Gradle Files" ボタンを手動で押すか、npx expo run:android のようにビルド前に自動的に実行されます。このフェーズで watchedDirectories をミラーするフォルダ構造が作成され、watchedDirectories 内の Kotlin ファイルへのシンボリックリンクが作られます。

このミラーフォルダ構造を作ることで:

  • ネイティブファイルがコンパイルされる: インラインモジュールは Android Studio から見えるようになり、Android ビルド時にコンパイルされる。
  • 他のファイルは無視される: ミラー内には JavaScript / TypeScript ファイルは含まれないため、Android Studio からはインデックスされない。

Autolinking

すべての Expo モジュールはグローバルオブジェクト上に存在し、ネイティブモジュールプロバイダを通じて公開されます。iOS と Android のビルド時にモジュールプロバイダクラスが生成され、通常の Expo モジュールとインラインモジュールの参照を含みます。JavaScript で requireNativeModule('InlineModule') を呼ぶと、このグローバルオブジェクトへのアクセスをラップしているだけです。

型生成の仕組み

型生成の基本アイデアはシンプルです: ネイティブモジュールの宣言は構造化されているため、ネイティブコードから直接 TypeScript インターフェースを生成できます。expo-type-information は以下の 4 つの主要コンポーネントで構成されます:

  • Swift パーサ
  • モジュール型の抽象化層
  • TypeScript コードジェネレータ
  • CLI ツール

これらが連携してモジュールの TypeScript インターフェース作成を自動化します。

型システム

DSL でモジュールを定義するとき、関数、定数、クラス、ビューなどのネイティブインターフェースを提供します。これらは Swift の型システムで厳密に型付けされています。一方で TypeScript 側は JavaScript オブジェクトを扱う TypeScript の型システム上で動作し、Swift / Kotlin とは根本的に異なり、厳密な 1 対 1 の対応は存在しません。したがって変換(conversions)を考慮する必要があります。

(ここで説明は続きますが、上記がインラインモジュールと expo-type-information の主要なポイントとその内部的な流れになります。)