Development Workflow
Compiled releases: supported subset and known differences
What a compiled release refuses to build, where it still behaves differently from the JavaScript release, and the versions it is tested with.
A compiled release runs your TypeScript as Swift and Kotlin, against a runtime that reproduces JavaScript's semantics. This page lists where that stops: what the build refuses, and the differences known today between a compiled release and the JavaScript release of the same app. Each entry names the platform it affects. Check every compiled release against its JavaScript release before you ship it (Checking a compiled release).
What the build refuses
Before anything is compiled, the compiler checks the app (and the plugin code the app reaches) and stops on each of these with the file, the line and what to write instead. All of them are reported at once.
| Construct | Why | Instead |
|---|---|---|
new Proxy(…) | No traps without a JavaScript engine | A class with explicit getters and setters, or a Map for dynamic keys |
Reflect.* | Not provided by the runtime | The operation itself: obj[key], key in obj, Object.keys(obj), new Cls(...args) |
eval(…), new Function(…) | There is no engine to evaluate code | A lookup of known functions; or keep this app on the JavaScript release |
FinalizationRegistry | Finalizers do not run | Release the resource explicitly (dispose(), unloaded, disposeNativeView) |
SharedArrayBuffer, Atomics | No shared memory between workers | An ArrayBuffer, with copies posted between workers |
with (obj) { … } | Name the object explicitly | |
import(expr), require(expr) with a computed specifier | Every module is linked when the app is built | Import each module by a literal path, and choose among those at run time |
arguments.callee, __proto__ | A named function; Object.getPrototypeOf | |
A bundler constant nothing defines (declare const __API_URL__, process.env.X) | It would read as undefined at run time | Define it in the bundler config: Vite's define and webpack's DefinePlugin values are read and compiled in |
The build also stops, before compiling, when:
- The app's
@nativescript/coreis not the core the compiler's kit was generated from. The kit is core's own code, compiled; the native widgets it drives come from your installed core, so the two must be the same release. Install the core version the message names, or the@nativescript/compilerreleased with your core.release: { allowCoreMismatch: true }builds anyway, with a warning. - A framework package is outside the versions its support is tested with (table below).
release: { allowUntestedFrameworks: true }builds anyway, with a warning. - The toolchain is missing or too old: Xcode 26 or newer for iOS; JDK 17 or newer and the Android SDK for Android.
Other unsupported language and framework constructs are refused by the compiler as it meets them, with the file and line; see What stops a build.
Tested framework versions
| Framework | Packages and versions |
|---|---|
| Vue | nativescript-vue 3.1 and later 3.x |
| Angular | @nativescript/angular and @angular/core 22.x |
| React | react-nativescript 5.x with react 18.2 or later 18.x |
| Solid | @nativescript-community/solid-js and solid-js 2.0 (from 2.0.0-rc.0) |
| Svelte | svelte-native 1.x with svelte 4.2+, or svelte-native 5 (from 5.0.0-alpha.0) with svelte 5.50+ |
| Octane | @nativescript-community/octane 0.2.4 and later 0.2.x |
Framework features each front end does not support yet stop the build with a message. The main ones:
- Angular: pipes other than
async; a pipe inside*ngFor/@for;OnPushunder zone.js; more than one@Componentin a file. Templates are parsed with the compiler's own@angular/compiler22. - React: a component body may hold function declarations, variable declarations and the
return; a bare statement such asuseEffect(() => …, [])on its own line is refused. JSX spread attributes are refused. - Vue:
<style module>, and Options API keys beyond the common ones.
Known differences at run time
These compile and run, and behave differently from the JavaScript release. They are being fixed; until then, avoid relying on them.
Values and types
nullandundefinedare one value in a typed optional (both platforms). Withb?: string | null, an explicitnullreads asundefined, disappears fromJSON.stringify, and a destructuring default applies to it. Code that tells=== nullfrom=== undefinedon such a field takes the wrong branch.- Fields of an
Errorsubclass read throughanyorunknownare undefined (both):catch (e: any) { e.code }. On Android, also afterinstanceof. - A value of the wrong type in a typed slot can stop the app instead of throwing a
TypeError(iOS): an untyped value (any, parsed JSON) assigned where a class or interface is declared, a downcast to the wrong subclass, a definitely-assigned field (x!: T) read before it is set, an out-of-union value in an exhaustiveswitch. Validate parsed data before typing it. - Number to integer conversions of NaN, Infinity or very large values can stop the app (iOS), where JavaScript gives 0 or wraps.
valueOf()is never called: arithmetic on objects concatenates or gives NaN (both).typeof SomeClassis"object"(both), andtypeof new String('x')is"string".- Subclass fields are initialized before the base constructor runs (both), so a base constructor that calls an overridden method sees the subclass's fields already set.
varin aforloop gets a new binding per iteration (both), asletdoes.
Strings and regular expressions
\d,\w,\band.follow Unicode rules (iOS):\dmatches Arabic-Indic digits,\wmatches accented letters.- A capture group that did not take part reads
'', notundefined(iOS). replacewith a string pattern ignores$&,$`,$'and$$(iOS).- Cutting a string between the two halves of a surrogate pair gives U+FFFD (iOS), so an emoji split and rejoined is corrupted.
str[i]past the end is'', notundefined(both).===on strings compares by Unicode canonical equivalence (iOS):'é'precomposed equals'e'+ accent, whileMapandSetkeys keep them apart.localeCompareignores its locale and options (both).lastIndexOf(x, fromIndex)ignoresfromIndex.
Arrays, dates, JSON
- Holes in a
number[]read as NaN (iOS).flat(depth)ignores the depth (iOS). Date.parseaccepts out-of-range ISO fields and rolls them over (iOS).JSON.parseignores a reviver (both).
Functions, listeners and memory
- A function read twice is not
===to itself (iOS), sooff(handler)andremoveEventListenerdo not remove a listener, and the same handler added twice runs twice. Keep the subscription handle where an API returns one. - Closures stored on an object, and parent and child views, hold each other strongly (iOS): a page navigated away from may stay in memory.
WeakMapandWeakSetlack ephemeron semantics (both): a value that reaches its key keeps both alive.WeakReftiming differs: on iOS a target can be freed within the job that created it; on Android aWeakRefis a soft reference and survives ordinary collections.- Script entered from a native background thread is not serialized with the main thread (both).
Errors and crashes
- Uncaught errors are printed, and
Application.on('uncaughtError')does not fire (both); theuncaughtErrorPolicysetting is not read. error.stacklists the native frames the error was made in, not JavaScript frames: on iOSsymbol (App+0xoffset), which the archive's dSYM resolves to your TypeScript lines; on Androidclass.method(File.kt:line), whichns-native-retracemaps to your TypeScript lines.- Deep recursion crashes (iOS) instead of throwing a
RangeError.
Android release builds (R8)
Members reached by name at run time (a method called on an any value, typeof x.getFoo, 'getFoo' in x) are kept from R8's renaming by a scan of the compiled code. A name computed at run time (x[name]() with name built from strings) is not found by the scan; keep such members with a ProGuard rule in App_Resources/Android/app.gradle, and check the minified release on a device.
By default every class and member of the kit is kept whole, which leaves most of the kit's code to R8 untouched. android: { release: { narrowKitKeeps: true } } keeps only what the kit's reflection needs (class names, constructors, the names of the members that remain) and lets R8 remove the rest: on Recipes (Vue) the kit's dex goes from 3.3 to 2.0 MB and the APK from 2.44 to 1.92 MB, with both screens matching the JavaScript release. It is opt-in until it has been verified on larger apps; under it, a kit member reached only through a computed name is removed.
- Previous
- Compiled releases
- Next
- Apple App Store
