Build1 publisher3 min readPublished
Expo Modules 2.0 reads a module's JavaScript surface off Swift declarations
In SDK 57 the DSL that listed what a module exposed becomes annotations on ordinary Swift methods, and Swift's own mutability and async keyword now decide what JavaScript sees. Views and Android come later.
The Engineer · Build desk

What happened
- Expo says that in Expo Modules 2.0 a module is an annotated Swift or Kotlin class, with the methods and properties you want in JavaScript simply marked as exposed.
- In SDK 58 the API goes to documented, official beta and ships with an agent skill that teaches a coding assistant how to write against it.
- One module can hold both APIs at once: keep the existing definition(), add @ExpoModule, and move functions and properties over to @JS individually.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Views have no 2.0 form and Android has not shipped one, so a cross-platform module that renders anything keeps the 1.0 DSL this cycle and carries two authoring styles in the same file.
- exposure The JavaScript API now moves when Swift declarations move, so dropping a setter or adding async changes the client contract without anyone editing something that looks like an API definition.
- cost The conversion work turns into diff review: the migrate skill rewrites the Swift side and reports only what it could not convert.
- precedent Expo is treating assistant instructions as a shipped part of the SDK 58 release.
The macro reads the declaration and nothing else. A method marked `@JS` becomes a JavaScript function whose name and types come off the Swift signature, and whether it is synchronous or Promise-returning is decided by Swift's `async` keyword [9]. Properties inherit Swift's mutability rules: a stored `var` or a computed property with a setter is writable from JS, and a `let` or a getter-only `var` is read-only [10]. There is no `definition()`, no `Name(...)` and no result builder, and the module name defaults to the class name unless you pass one as a macro argument [12].
So the JavaScript contract is now a side effect of Swift declaration details. In 1.0 the surface sat in one block written in a small grammar of its own, built on result builders, with `Function`, `AsyncFunction`, `Property`, `Class` and `Events` as its vocabulary and closure parameter types written out by hand [11]. In 2.0, adding `async` to a Swift method turns the JS call into a promise [9], a one-keyword diff in a file a reviewer may not treat as an API file.
Expo's post says 2.0 brings "Nothing new to learn beyond a handful of annotations, no boilerplate to maintain, and it's faster at runtime than the API it replaces" [8]. The speed numbers in that post belong to the layer underneath the authoring change, and the post gives no figure for the 2.0 authoring change on its own. On iOS, SDK 56 dropped the Objective-C++ shim so Swift talks to JSI directly and calls are "roughly twice as fast" [6]. On Android, a Kotlin compiler plugin moved some work from runtime to build time and cut time to first render by 30% [7]. For the SDK 56 factor to show up in your app, boundary crossings have to be your hot path; a module that makes one call and then does eighty milliseconds of native work will not feel it.
Migration is incremental by construction. Add `@ExpoModule`, keep the existing `definition()`, and move functions and properties to `@JS` one at a time, because the two descriptions are merged and anything without a 2.0 form stays in the definition [13]. In SDK 57 the macros cover modules, functions, records, shared objects and events, five of the six construct kinds the post names, and views are the one left out [3][16]. Android is still in the works and will follow the same authoring model [4]. A cross-platform module with a view keeps its 1.0 code and gains 2.0 annotations beside it.
The Swift side can be handed off: the `expo-migrate-module` skill migrates a module from 1.0 to 2.0 with the JavaScript API unchanged, leaves anything it cannot convert in `definition()`, and lists those in its report [14]. Expo's case for the change is partly about context budget, describing the 1.0 DSL as "a fair amount to keep in your head, or in your coding agent's context" [15]. Counting from the JSI work, the interop rework will have run across three consecutive releases, SDK 56 through 58, by the time 2.0 reaches beta [17]. The post covers Expo Modules 1.0 to 2.0 and does not discuss migrating React Native bridge or Turbo Module code.
What to watch
- Whether Android's Kotlin annotations land with the same @JS surface, and in which SDK release.
- Whether views get a 2.0 form before the SDK 58 beta, or stay in definition() past it.
- Whether the SDK 58 beta keeps the merged 1.0/2.0 path or starts deprecating definition().